Storage limit: 80% warning and limit in the web interface
CI / unit (8.2) (push) Has been cancelled
CI / unit (8.3) (push) Has been cancelled
CI / unit (8.4) (push) Has been cancelled
CI / integration (mariadb, 32) (push) Has been cancelled
CI / integration (mariadb, 33) (push) Has been cancelled
CI / integration (mariadb, 34) (push) Has been cancelled
CI / integration (mariadb, 35) (push) Has been cancelled
CI / integration (pgsql, 32) (push) Has been cancelled
CI / integration (pgsql, 33) (push) Has been cancelled
CI / integration (pgsql, 34) (push) Has been cancelled
CI / integration (pgsql, 35) (push) Has been cancelled
CI / integration (sqlite, 32) (push) Has been cancelled
CI / integration (sqlite, 33) (push) Has been cancelled
CI / integration (sqlite, 34) (push) Has been cancelled
CI / integration (sqlite, 35) (push) Has been cancelled

- after each successful backup the stored size is recorded; at 80% of a
  location's limit administrators get a notification (once a day)
- locations list shows stored size and limit; the add form has a
  'maximum size' field that explains that empty means unlimited and
  billed by the provider; PUT /api/targets/{id}/limit (password
  confirmation)

Tested on NC34 with the OVH test bucket: a backup above the limit stops,
a successful one at 81% sends the warning.
This commit is contained in:
2026-10-02 14:50:16 +02:00
parent 2610c1ad2f
commit 85e835bfc2
8 changed files with 70 additions and 7 deletions
+1
View File
@@ -29,6 +29,7 @@ export default {
addTarget: (target) => call('post', '/targets', target),
removeTarget: (id) => call('delete', `/targets/${id}`),
testTarget: (id) => call('post', `/targets/${id}/test`),
setTargetLimit: (id, maxGb) => call('put', `/targets/${id}/limit`, { maxGb }),
startBackup: (targetId, label) => call('post', '/backups', { targetId, label }),
setSchedule: (time) => call('put', '/schedule', { time }),
snapshots: (targetId) => call('get', `/targets/${targetId}/snapshots`),
+19 -4
View File
@@ -8,7 +8,8 @@
<ul v-if="status.targets.length" class="ngb-list">
<li v-for="target in status.targets" :key="target.id">
<strong>{{ target.name }}</strong>
<span class="ngb-muted">{{ target.backend }} · {{ target.path }}</span>
<span class="ngb-muted">{{ target.backend }} · {{ target.path }}{{ target.appendOnly ? ' · ' + t('ng_backup', 'append-only') : '' }}</span>
<span :class="usageClass(target)">{{ usageText(target) }}</span>
<span v-if="tests[target.id]" :class="tests[target.id].ok ? 'ngb-ok' : 'ngb-err'">
{{ tests[target.id].ok ? t('ng_backup', 'OK, {n} snapshots', { n: tests[target.id].snapshots }) : tests[target.id].error }}
</span>
@@ -28,6 +29,8 @@
<NcTextField v-else v-model="form.options[field.key]" :label="field.label + (field.optional ? '' : ' *')" />
</template>
<NcTextField v-model="form.path" :label="t('ng_backup', 'Folder on the location for the backups')" />
<NcTextField v-model="form.maxGb" type="number" min="0" :label="t('ng_backup', 'Maximum size in GiB')"
:helper-text="form.maxGb ? t('ng_backup', 'Backups stop at this size; administrators are warned at 80%.') : t('ng_backup', 'Empty = unlimited: everything stored is billed by your provider, and nothing stops a backup that keeps growing.')" />
<div class="ngb-row">
<NcButton type="submit" variant="primary" :disabled="busy || !form.name || !backend || !auth">{{ t('ng_backup', 'Connect and add') }}</NcButton>
<NcButton @click="adding = false">{{ t('ng_backup', 'Cancel') }}</NcButton>
@@ -61,7 +64,18 @@ const backends = ref([])
const backend = ref(null)
const auth = ref(null)
const tests = reactive({})
const form = reactive({ name: '', path: 'ng_backup', options: {} })
const form = reactive({ name: '', path: 'ng_backup', options: {}, maxGb: '' })
const gib = (b) => (b / 1073741824).toFixed(1) + ' GiB'
function usageText(target) {
if (target.storedBytes === null || target.storedBytes === undefined) {
return target.maxBytes ? t('ng_backup', 'limit {limit}', { limit: gib(target.maxBytes) }) : t('ng_backup', 'no limit')
}
return target.maxBytes
? t('ng_backup', '{used} of {limit}', { used: gib(target.storedBytes), limit: gib(target.maxBytes) })
: t('ng_backup', '{used} stored, no limit', { used: gib(target.storedBytes) })
}
const usageClass = (target) => (target.maxBytes && target.storedBytes >= 0.8 * target.maxBytes) ? 'ngb-err' : 'ngb-muted'
// files_external DefinitionParameter: type 0 text, 1 boolean, 2 password; flag 1 = optional, 4 = hidden
const toFields = (params) => Object.entries(params || {})
@@ -90,9 +104,10 @@ async function add() {
busy.value = true
error.value = ''
try {
await api.addTarget({ name: form.name, backend: backend.value.id, auth: auth.value.id, options: form.options, path: form.path })
await api.addTarget({ name: form.name, backend: backend.value.id, auth: auth.value.id, options: form.options, path: form.path,
maxGb: form.maxGb === '' ? null : Number(form.maxGb) })
adding.value = false
Object.assign(form, { name: '', path: 'ng_backup', options: {} })
Object.assign(form, { name: '', path: 'ng_backup', options: {}, maxGb: '' })
emit('changed')
} catch (e) {
error.value = e.message