diff --git a/src/lib/components/backupDatabaseAlert.svelte b/src/lib/components/backupDatabaseAlert.svelte
new file mode 100644
index 000000000..d6fa60365
--- /dev/null
+++ b/src/lib/components/backupDatabaseAlert.svelte
@@ -0,0 +1,46 @@
+
+
+{#if $showPolicyAlert && isCloud && $organization?.$id && $page.url.pathname.match(/\/databases\/database-[^/]+$/)}
+ {@const isFreePlan = $organization?.billingPlan === BillingPlan.FREE}
+
+ {@const subtitle = isFreePlan
+ ? 'Upgrade your plan to ensure your data stays safe and backed up'
+ : 'Protect your data by quickly adding a backup policy'}
+
+ {@const ctaText = isFreePlan ? 'Upgrade plan' : 'Add backup'}
+ {@const ctaURL = isFreePlan ? $upgradeURL : `${$page.url.pathname}/backups`}
+
+
+ {subtitle}
+
+
+
+
+
+
+
+
+{/if}
diff --git a/src/lib/components/backupRestoreBox.svelte b/src/lib/components/backupRestoreBox.svelte
new file mode 100644
index 000000000..842c15e19
--- /dev/null
+++ b/src/lib/components/backupRestoreBox.svelte
@@ -0,0 +1,257 @@
+
+
+{#if showBackupRestoreBox}
+
+ {#each Object.keys(backupRestoreItems) as key}
+ {@const isBackup = key === 'archives'}
+ {@const items = backupRestoreItems[key]}
+ {@const titleText = isBackup ? 'Backup status' : 'Restoration status'}
+
+ {#if items.size > 0}
+
+
+
+
+
+ {#each [...items.values()] as item (item.$id)}
+ -
+
+
+
+ {text(item.status, key)}
+
+
+
+ {backupName(item, key)}
+
+
+
+
+
+ {/each}
+
+
+
+ {/if}
+ {/each}
+
+{/if}
+
+
diff --git a/src/lib/components/drop.svelte b/src/lib/components/drop.svelte
index 2d4ee2b73..db5eac60e 100644
--- a/src/lib/components/drop.svelte
+++ b/src/lib/components/drop.svelte
@@ -42,7 +42,7 @@
{
name: 'offset',
options: {
- offset: [noArrow ? 0 : -arrowSize, noArrow ? 0 : arrowSize / 1.5]
+ offset: [noArrow ? 0 : arrowSize * 1.75, noArrow ? 0 : arrowSize / 1.5]
}
},
{
@@ -135,10 +135,12 @@
diff --git a/src/lib/components/modal.svelte b/src/lib/components/modal.svelte
index 410116586..c043fc1dc 100644
--- a/src/lib/components/modal.svelte
+++ b/src/lib/components/modal.svelte
@@ -70,7 +70,7 @@
{/if}
{#if description.length > 0}
-
+
{description}
diff --git a/src/lib/components/uploadBox.svelte b/src/lib/components/uploadBox.svelte
index 0d744fd65..8cd9883cc 100644
--- a/src/lib/components/uploadBox.svelte
+++ b/src/lib/components/uploadBox.svelte
@@ -13,7 +13,7 @@
{#if $uploader?.isOpen}
-
+
{/if}
+
+
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index e73ba5854..ffa1bda99 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -50,6 +50,7 @@ export enum Dependencies {
WEBHOOKS = 'dependency:webhooks',
MIGRATIONS = 'dependency:migrations',
COLLECTIONS = 'dependency:collections',
+ BACKUPS = 'dependency:backups',
RUNTIMES = 'dependency:runtimes',
CONSOLE_VARIABLES = 'dependency:console_variables',
MESSAGING_PROVIDERS = 'dependency:messaging_providers',
diff --git a/src/lib/elements/forms/inputSelectCheckbox.svelte b/src/lib/elements/forms/inputSelectCheckbox.svelte
index b4f735b32..57c0fd440 100644
--- a/src/lib/elements/forms/inputSelectCheckbox.svelte
+++ b/src/lib/elements/forms/inputSelectCheckbox.svelte
@@ -68,7 +68,7 @@
@@ -86,3 +86,15 @@
{/each}
+
+
diff --git a/src/lib/helpers/backups.ts b/src/lib/helpers/backups.ts
new file mode 100644
index 000000000..0921b15a4
--- /dev/null
+++ b/src/lib/helpers/backups.ts
@@ -0,0 +1,165 @@
+export type UserBackupPolicy = {
+ id?: string;
+ label: string;
+ retained: number;
+ default: boolean;
+ description: string;
+
+ schedule?: string;
+
+ checked?: boolean;
+ selectedTime?: string;
+ plainTextFrequency?: string;
+ weeklySelectedDays?: string[];
+ monthlyBackupFrequency?: string;
+};
+
+export const cronExpression = (policy: UserBackupPolicy) => {
+ const now = new Date();
+
+ if (policy.plainTextFrequency === 'hourly') {
+ const utcMinute = now.getUTCMinutes();
+
+ if (!policy.default) {
+ policy.schedule = `${utcMinute} * * * *`;
+ }
+ return;
+ }
+
+ let cronExpression = '';
+
+ if (policy.default) {
+ // default should use utc.
+ cronExpression = policy.schedule;
+ } else {
+ const [localHour, localMinute] = policy.selectedTime.split(':');
+ now.setHours(parseInt(localHour), parseInt(localMinute), 0);
+
+ const utcHour = now.getUTCHours();
+ const utcMinute = now.getUTCMinutes();
+
+ if (policy.plainTextFrequency === 'daily') {
+ cronExpression = `${utcMinute} ${utcHour} * * *`;
+ } else if (policy.plainTextFrequency === 'weekly') {
+ const selectedDays = policy.weeklySelectedDays
+ ?.map(
+ (dayLabel) =>
+ backupFrequencies.weekly.find((option) => option.label === dayLabel)?.index
+ )
+ .filter((index) => index !== undefined)
+ .join(',');
+
+ // Default to Monday (1)
+ cronExpression = `${utcMinute} ${utcHour} * * ${selectedDays || '1'}`;
+ } else if (policy.plainTextFrequency === 'monthly') {
+ cronExpression = `${utcMinute} ${utcHour} 28 * *`;
+ }
+ }
+
+ policy.schedule = cronExpression;
+};
+
+const generateHourlyOptions = (start: number, end: number) =>
+ Array.from({ length: end - start + 1 }, (_, i) => ({
+ value: `${i + start}`,
+ label: `${i + start}`
+ }));
+
+const generateWeeklyOptions = () =>
+ ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'].map(
+ (day, index) => ({
+ label: day,
+ value: day,
+ index: index,
+ checked: false
+ })
+ );
+
+export const backupFrequencies = {
+ monthly: [
+ { value: 'first', label: 'First day of month', day: '1' },
+ { value: 'middle', label: 'Middle of month (15th)', day: '15' },
+ { value: 'end', label: 'End of month (28th)', day: '28' }
+ ],
+ weekly: generateWeeklyOptions(),
+ hourly: generateHourlyOptions(1, 12)
+};
+
+export const backupPolicyDescription = (
+ frequency: string,
+ time: string | null = null,
+ retained: number | null = null,
+ monthlyBackupFrequency: string,
+ weeklySelectedDays: string[] | null = null
+) => {
+ let retainedText = '';
+ const timeFormatted = time ?? '';
+
+ if (retained !== null) {
+ if (retained === 365 * 100) {
+ retainedText = 'forever';
+ } else if (retained >= 365) {
+ const years = Math.floor(retained / 365);
+ retainedText = `${years} year${years > 1 ? 's' : ''}`;
+ } else if (retained >= 30) {
+ const months = Math.floor(retained / 30);
+ retainedText = `${months} month${months > 1 ? 's' : ''}`;
+ } else if (retained >= 7) {
+ const weeks = Math.floor(retained / 7);
+ retainedText = `${weeks} week${weeks > 1 ? 's' : ''}`;
+ } else {
+ retainedText = `${retained} day${retained > 1 ? 's' : ''}`;
+ }
+ }
+
+ switch (frequency) {
+ case 'hourly':
+ return retained !== null
+ ? `Runs every hour and is retained for ${retainedText}.`
+ : 'A backup will run every hour.';
+
+ case 'daily':
+ return retained !== null
+ ? `Runs every day and is retained for ${retainedText}.`
+ : `A backup will run daily at ${timeFormatted}.`;
+
+ case 'weekly': {
+ const daysArray = weeklySelectedDays.length ? weeklySelectedDays : ['Monday'];
+ const dayString =
+ daysArray.length > 1
+ ? daysArray.slice(0, -1).join(', ') + ' and ' + daysArray.slice(-1)
+ : daysArray[0];
+ return retained !== null
+ ? `Runs every ${dayString} and is retained for ${retainedText}.`
+ : `A backup will run weekly on ${dayString} at ${timeFormatted}.`;
+ }
+
+ case 'monthly': {
+ const monthDay =
+ backupFrequencies[frequency]
+ .find((option) => option.value === monthlyBackupFrequency)
+ ?.label.toLowerCase() || '28th';
+
+ let actualDay: string;
+ switch (monthlyBackupFrequency) {
+ case 'first':
+ actualDay = '1st';
+ break;
+ case 'middle':
+ actualDay = '15th';
+ break;
+ case 'end':
+ default:
+ actualDay = '28th';
+ break;
+ }
+
+ return retained !== null
+ ? `Runs every ${actualDay} of the month and is retained for ${retainedText}.`
+ : `A backup will run every month on the ${monthDay} at ${timeFormatted}.`;
+ }
+
+ default:
+ return 'A backup schedule is not set.';
+ }
+};
diff --git a/src/lib/helpers/notifications.ts b/src/lib/helpers/notifications.ts
index 306a80995..e5f9ae143 100644
--- a/src/lib/helpers/notifications.ts
+++ b/src/lib/helpers/notifications.ts
@@ -18,7 +18,8 @@ const userPreferences = () => get(user).prefs;
const notificationPrefs = (): Record => {
const prefs = userPreferences();
- return prefs.notificationPrefs ? prefs.notificationPrefs : {};
+ // for some reason, the prefs become array as default or on all clear. let's reset.
+ return Array.isArray(prefs.notificationPrefs) ? {} : prefs.notificationPrefs || {};
};
function updateNotificationPrefs(parsedPrefs: Record) {
diff --git a/src/lib/images/backups/backups-dark.png b/src/lib/images/backups/backups-dark.png
new file mode 100644
index 000000000..fcf87eff8
Binary files /dev/null and b/src/lib/images/backups/backups-dark.png differ
diff --git a/src/lib/images/backups/backups-light.png b/src/lib/images/backups/backups-light.png
new file mode 100644
index 000000000..220d6c391
Binary files /dev/null and b/src/lib/images/backups/backups-light.png differ
diff --git a/src/lib/images/backups/empty/backups-dark.png b/src/lib/images/backups/empty/backups-dark.png
new file mode 100644
index 000000000..333651538
Binary files /dev/null and b/src/lib/images/backups/empty/backups-dark.png differ
diff --git a/src/lib/images/backups/empty/backups-light.png b/src/lib/images/backups/empty/backups-light.png
new file mode 100644
index 000000000..888e662ad
Binary files /dev/null and b/src/lib/images/backups/empty/backups-light.png differ
diff --git a/src/lib/images/backups/empty/backups-mobile-dark.png b/src/lib/images/backups/empty/backups-mobile-dark.png
new file mode 100644
index 000000000..7c9000e8a
Binary files /dev/null and b/src/lib/images/backups/empty/backups-mobile-dark.png differ
diff --git a/src/lib/images/backups/empty/backups-mobile-light.png b/src/lib/images/backups/empty/backups-mobile-light.png
new file mode 100644
index 000000000..1d0255f45
Binary files /dev/null and b/src/lib/images/backups/empty/backups-mobile-light.png differ
diff --git a/src/lib/images/backups/empty/backups-tablet-dark.png b/src/lib/images/backups/empty/backups-tablet-dark.png
new file mode 100644
index 000000000..45d4dc86f
Binary files /dev/null and b/src/lib/images/backups/empty/backups-tablet-dark.png differ
diff --git a/src/lib/images/backups/empty/backups-tablet-light.png b/src/lib/images/backups/empty/backups-tablet-light.png
new file mode 100644
index 000000000..5a9141f74
Binary files /dev/null and b/src/lib/images/backups/empty/backups-tablet-light.png differ
diff --git a/src/lib/images/backups/promo/backups-dark.png b/src/lib/images/backups/promo/backups-dark.png
new file mode 100644
index 000000000..6b31eab88
Binary files /dev/null and b/src/lib/images/backups/promo/backups-dark.png differ
diff --git a/src/lib/images/backups/promo/backups-light.png b/src/lib/images/backups/promo/backups-light.png
new file mode 100644
index 000000000..aa675d86b
Binary files /dev/null and b/src/lib/images/backups/promo/backups-light.png differ
diff --git a/src/lib/images/backups/upgrade/backups-dark.png b/src/lib/images/backups/upgrade/backups-dark.png
new file mode 100644
index 000000000..2ce08b804
Binary files /dev/null and b/src/lib/images/backups/upgrade/backups-dark.png differ
diff --git a/src/lib/images/backups/upgrade/backups-light.png b/src/lib/images/backups/upgrade/backups-light.png
new file mode 100644
index 000000000..ba64a7877
Binary files /dev/null and b/src/lib/images/backups/upgrade/backups-light.png differ
diff --git a/src/lib/images/backups/upgrade/backups-mobile-dark.png b/src/lib/images/backups/upgrade/backups-mobile-dark.png
new file mode 100644
index 000000000..1e3046557
Binary files /dev/null and b/src/lib/images/backups/upgrade/backups-mobile-dark.png differ
diff --git a/src/lib/images/backups/upgrade/backups-mobile-light.png b/src/lib/images/backups/upgrade/backups-mobile-light.png
new file mode 100644
index 000000000..bb476939b
Binary files /dev/null and b/src/lib/images/backups/upgrade/backups-mobile-light.png differ
diff --git a/src/lib/images/backups/upgrade/backups-tablet-dark.png b/src/lib/images/backups/upgrade/backups-tablet-dark.png
new file mode 100644
index 000000000..2ad9c5a57
Binary files /dev/null and b/src/lib/images/backups/upgrade/backups-tablet-dark.png differ
diff --git a/src/lib/images/backups/upgrade/backups-tablet-light.png b/src/lib/images/backups/upgrade/backups-tablet-light.png
new file mode 100644
index 000000000..34fb4ea5f
Binary files /dev/null and b/src/lib/images/backups/upgrade/backups-tablet-light.png differ
diff --git a/src/lib/layout/headerAlert.svelte b/src/lib/layout/headerAlert.svelte
index f99449110..7ab5a58af 100644
--- a/src/lib/layout/headerAlert.svelte
+++ b/src/lib/layout/headerAlert.svelte
@@ -10,7 +10,13 @@
class:is-danger={type === 'error'}
class:is-info={type === 'info'}>
-
+
+
{#if title || $$slots.title}
@@ -30,3 +36,13 @@
{/if}
+
+
diff --git a/src/lib/sdk/backups.ts b/src/lib/sdk/backups.ts
new file mode 100644
index 000000000..43cb6eb8b
--- /dev/null
+++ b/src/lib/sdk/backups.ts
@@ -0,0 +1,332 @@
+import { AppwriteException, Client, type Payload } from '@appwrite.io/console';
+
+export type BackupPolicyList = {
+ total: number;
+ policies: BackupPolicy[];
+};
+
+export type BackupArchiveList = {
+ total: number;
+ archives: BackupArchive[];
+};
+
+export type BackupRestorationList = {
+ total: number;
+ restorations: BackupRestoration[];
+};
+
+export type BackupPolicy = {
+ $id: string;
+ name: string;
+ $createdAt: string;
+ $updatedAt: string;
+ services: string[];
+ resources: string[];
+ resourceId?: string;
+ resourceType?: string;
+ retention: number;
+ schedule: string;
+ enabled: boolean;
+};
+
+export type BackupArchive = {
+ $id: string;
+ $createdAt: string;
+ $updatedAt: string;
+ policyId: string;
+ size: number;
+ status: string;
+ startedAt: string;
+ migrationId: string;
+ services: string[];
+ resources: string[];
+ resourceId?: string;
+ resourceType?: string;
+};
+
+export type BackupRestoration = {
+ $id: string;
+ $createdAt: string;
+ $updatedAt: string;
+ archiveId: string;
+ policyId: string;
+ status: string;
+ startedAt: string;
+ migrationId: string;
+ services: string[];
+ resources: string[];
+ options: string;
+};
+
+export class Backups {
+ client: Client;
+
+ constructor(client: Client) {
+ this.client = client;
+ }
+
+ async createArchive(services: string[], resourceId?: string): Promise
{
+ if (typeof services === 'undefined') {
+ throw new AppwriteException('Missing required parameter: "services"');
+ }
+ const apiPath = '/backups/archives';
+ const payload: Payload = {};
+ if (typeof services !== 'undefined') {
+ payload['services'] = services;
+ }
+ if (typeof resourceId !== 'undefined') {
+ payload['resourceId'] = resourceId;
+ }
+ const uri = new URL(this.client.config.endpoint + apiPath);
+
+ const apiHeaders: { [header: string]: string } = {
+ 'content-type': 'application/json'
+ };
+
+ return await this.client.call('post', uri, apiHeaders, payload);
+ }
+
+ async deleteArchive(archiveId: string): Promise