feat(maintenance): add a maintenance notification during backend migration
This commit is contained in:
@@ -18,6 +18,15 @@
|
||||
*/
|
||||
|
||||
import { getCsrfHeaders } from './csrf';
|
||||
import { updateFromHeader } from '$lib/stores/serverStatus.svelte';
|
||||
|
||||
/**
|
||||
* Name of the response header the server stamps while a
|
||||
* maintenance event is live. Case-insensitive on the wire — the
|
||||
* Fetch API's `Headers.get` matches irrespective of case, so this
|
||||
* constant matches whatever axum emits.
|
||||
*/
|
||||
const SERVER_STATUS_HEADER = 'x-server-status';
|
||||
|
||||
const REFRESH_ENDPOINT = '/api/auth/refresh';
|
||||
|
||||
@@ -93,6 +102,18 @@ export function createApiFetch(deps: ApiClientDeps): FetchFn {
|
||||
const apiFetch: FetchFn = async (input, init) => {
|
||||
const origin = deps.origin ?? globalThis.location?.origin ?? 'http://localhost';
|
||||
const response = await rawFetch(input, init);
|
||||
// Server-status header piggyback — the server stamps
|
||||
// `x-server-status` on every response while a maintenance
|
||||
// event is in progress (see middleware::server_status). Read
|
||||
// it and update the reactive store; the AppShell banner
|
||||
// subscribes and shows/hides itself. Absent header = nothing
|
||||
// happening; the update fn resets the store to default in
|
||||
// that case so a lingering banner disappears.
|
||||
//
|
||||
// Runs on EVERY response including a 401 (below) so a session
|
||||
// refresh doesn't accidentally clear a live banner.
|
||||
updateFromHeader(response.headers.get(SERVER_STATUS_HEADER));
|
||||
|
||||
if (response.status !== 401) return response;
|
||||
|
||||
const urlStr = urlString(input as RequestInfo | URL);
|
||||
@@ -104,7 +125,9 @@ export function createApiFetch(deps: ApiClientDeps): FetchFn {
|
||||
onSessionExpired();
|
||||
throw new Error('Session expired');
|
||||
}
|
||||
return rawFetch(input, init);
|
||||
const retryResponse = await rawFetch(input, init);
|
||||
updateFromHeader(retryResponse.headers.get(SERVER_STATUS_HEADER));
|
||||
return retryResponse;
|
||||
};
|
||||
|
||||
return apiFetch;
|
||||
|
||||
@@ -11,10 +11,12 @@
|
||||
import type { FileItem, FolderItem, ItemType } from '$lib/api/types';
|
||||
import { lazyComponent } from '$lib/composables/lazyComponent.svelte';
|
||||
import DrivePicker from '$lib/components/DrivePicker.svelte';
|
||||
import ReadOnlyBanner from '$lib/components/ReadOnlyBanner.svelte';
|
||||
import Icon from '$lib/icons/Icon.svelte';
|
||||
import { dateTimeFormatFor, iconNameFromClass } from '$lib/utils/display';
|
||||
import { userInitials, avatarColorIndex } from '$lib/utils/avatar';
|
||||
import { i18n, LANGUAGES, setLocale, t, type Locale } from '$lib/i18n/index.svelte';
|
||||
import { serverStatus } from '$lib/stores/serverStatus.svelte';
|
||||
import { apiFetch } from '$lib/api/client';
|
||||
import { dialogs } from '$lib/stores/dialogs.svelte';
|
||||
import { files as filesStore } from '$lib/stores/files.svelte';
|
||||
@@ -1025,6 +1027,21 @@
|
||||
</div>
|
||||
|
||||
<div class="content-area">
|
||||
<!-- Server-wide maintenance banner. Fed by the
|
||||
`x-server-status` header read on every API response by
|
||||
`apiFetch` — no polling. Shows for every logged-in user
|
||||
while a storage migration is running so they know why
|
||||
writes are being refused, with live progress if
|
||||
available. Disappears automatically on the next API
|
||||
round-trip after the server clears the flag.
|
||||
|
||||
Reuses `ReadOnlyBanner` (same component that renders a
|
||||
drive-frozen notice) with `variant="maintenance"` so the
|
||||
two banners are visually indistinguishable — just
|
||||
different copy. -->
|
||||
{#if serverStatus().readonly}
|
||||
<ReadOnlyBanner variant="maintenance" progress={serverStatus().migration} />
|
||||
{/if}
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Read-only drive banner.
|
||||
* Read-only banner — one component, two variants.
|
||||
*
|
||||
* ## `variant="drive"` (default) — drive-scoped freeze
|
||||
*
|
||||
* Rendered at the top of any page whose content lives in (or is scoped
|
||||
* to) a drive whose `policies.read_only === true`. Members see the
|
||||
@@ -19,33 +21,60 @@
|
||||
* - `routes/files/[...path]/+page.svelte` — shown when the current
|
||||
* folder's owning drive is frozen (parent looks up drive via
|
||||
* `drives.findByRootFolderId`/`findById`).
|
||||
* - Future: `/photos`, `/music`, and any other drive-scoped views.
|
||||
*
|
||||
* ## `variant="maintenance"` — server-wide freeze
|
||||
*
|
||||
* Rendered inside `AppShell` above `{children}` when the
|
||||
* `x-server-status` header (see `middleware::server_status`) says
|
||||
* the whole server is in read-only mode — typically during a
|
||||
* storage-backend migration. Optional `progress` lets the banner
|
||||
* show target + percentage.
|
||||
*
|
||||
* Shape / accent is identical between both variants — the design
|
||||
* system reads them as the same family. Only the copy differs.
|
||||
*/
|
||||
import { t } from '$lib/i18n/index.svelte';
|
||||
import Icon from '$lib/icons/Icon.svelte';
|
||||
|
||||
interface Props {
|
||||
/** Drive-name shown in the body so members know which drive the
|
||||
* freeze applies to. Optional — omit on pages where the drive is
|
||||
* implicit from context (e.g. the drive's own config page). */
|
||||
driveName?: string;
|
||||
interface Progress {
|
||||
target: string;
|
||||
migrated: number;
|
||||
total: number;
|
||||
percent: number;
|
||||
}
|
||||
|
||||
let { driveName }: Props = $props();
|
||||
interface Props {
|
||||
/**
|
||||
* `"drive"` — a specific drive is frozen (default; back-compat
|
||||
* with pre-migration call sites). `"maintenance"` — the whole
|
||||
* server is in read-only mode.
|
||||
*/
|
||||
variant?: 'drive' | 'maintenance';
|
||||
/** Drive-name shown in the body (variant="drive" only). */
|
||||
driveName?: string;
|
||||
/** Migration progress (variant="maintenance" only). */
|
||||
progress?: Progress;
|
||||
}
|
||||
|
||||
let { variant = 'drive', driveName, progress }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="read-only-banner"
|
||||
role="region"
|
||||
aria-label={t('drive.read_only_banner.aria', 'This drive is read-only')}
|
||||
data-testid="read-only-banner"
|
||||
aria-label={variant === 'maintenance'
|
||||
? t('server_status.readonly_banner_aria', 'Server maintenance in progress')
|
||||
: t('drive.read_only_banner.aria', 'This drive is read-only')}
|
||||
data-testid={variant === 'maintenance' ? 'server-status-banner' : 'read-only-banner'}
|
||||
>
|
||||
<div class="read-only-banner__icon" aria-hidden="true">
|
||||
<Icon name="lock" />
|
||||
</div>
|
||||
<div class="read-only-banner__body">
|
||||
<strong>
|
||||
{#if driveName}
|
||||
{#if variant === 'maintenance'}
|
||||
{t('server_status.readonly_title', 'Server maintenance in progress')}
|
||||
{:else if driveName}
|
||||
{t(
|
||||
'drive.read_only_banner.title_named',
|
||||
{ name: driveName },
|
||||
@@ -56,10 +85,30 @@
|
||||
{/if}
|
||||
</strong>
|
||||
<span>
|
||||
{t(
|
||||
'drive.read_only_banner.body',
|
||||
'Uploads, edits, deletes, renames, sharing and membership changes are refused. Reads and downloads keep working. Contact an administrator to un-freeze the drive.'
|
||||
)}
|
||||
{#if variant === 'maintenance'}
|
||||
{#if progress}
|
||||
{t(
|
||||
'server_status.readonly_progress',
|
||||
{
|
||||
target: progress.target,
|
||||
migrated: progress.migrated,
|
||||
total: progress.total,
|
||||
percent: progress.percent
|
||||
},
|
||||
'Migrating storage to `{{target}}` — {{percent}}% ({{migrated}} / {{total}} blobs). Uploads, renames, deletes, and shares are refused; reads and downloads work as normal.'
|
||||
)}
|
||||
{:else}
|
||||
{t(
|
||||
'server_status.readonly_body',
|
||||
'Uploads, renames, deletes, and shares are refused temporarily. Reads and downloads work as normal.'
|
||||
)}
|
||||
{/if}
|
||||
{:else}
|
||||
{t(
|
||||
'drive.read_only_banner.body',
|
||||
'Uploads, edits, deletes, renames, sharing and membership changes are refused. Reads and downloads keep working. Contact an administrator to un-freeze the drive.'
|
||||
)}
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Reactive server-status store.
|
||||
*
|
||||
* Populated by the `apiFetch` wrapper, which reads the
|
||||
* `x-server-status` header off every API response and calls
|
||||
* `updateFromHeader(...)`. When no migration is running the header
|
||||
* is absent and the store stays at its default (readonly=false, no
|
||||
* migration info). See `middleware::server_status` on the server
|
||||
* for the header spec.
|
||||
*
|
||||
* The AppShell subscribes to this store to show/hide the
|
||||
* maintenance banner without polling — the state travels back to
|
||||
* the client on the piggyback of whatever API request the user was
|
||||
* making anyway. Zero extra network cost.
|
||||
*/
|
||||
|
||||
/**
|
||||
* JSON shape emitted in the `x-server-status` header. Optional
|
||||
* `migration` field is present only while a migration is running.
|
||||
*/
|
||||
export interface ServerStatus {
|
||||
readonly: boolean;
|
||||
migration?: {
|
||||
target: string;
|
||||
migrated: number;
|
||||
total: number;
|
||||
percent: number;
|
||||
};
|
||||
}
|
||||
|
||||
const DEFAULT: ServerStatus = { readonly: false };
|
||||
|
||||
// Rune-based reactive state — `$state` in a `.svelte.ts` module.
|
||||
let current = $state<ServerStatus>(DEFAULT);
|
||||
|
||||
/** Current server status. Reactively updates when apiFetch sees a new header. */
|
||||
export function serverStatus(): ServerStatus {
|
||||
return current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the raw header value and update the store. Silently
|
||||
* tolerates a missing header (resets to default: nothing to
|
||||
* broadcast means nothing wrong) and a malformed one (keeps the
|
||||
* previous value rather than surface a parse error to users).
|
||||
*
|
||||
* Called by `apiFetch` after every response — see `client.ts`.
|
||||
*/
|
||||
export function updateFromHeader(rawHeader: string | null): void {
|
||||
if (rawHeader == null) {
|
||||
// No header on this response = server not in maintenance
|
||||
// mode = reset the store to the default so any lingering
|
||||
// banner disappears. Cheap idempotent write.
|
||||
if (current.readonly || current.migration) current = DEFAULT;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(rawHeader) as ServerStatus;
|
||||
// Basic shape validation — server should never send a
|
||||
// missing `readonly`, but be defensive.
|
||||
if (typeof parsed.readonly === 'boolean') {
|
||||
current = parsed;
|
||||
}
|
||||
} catch {
|
||||
// Malformed header — keep previous state rather than churn.
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"server_status": {
|
||||
"readonly_banner_aria": "صيانة الخادم جارية",
|
||||
"readonly_title": "صيانة الخادم جارية",
|
||||
"readonly_progress": "جارٍ نقل التخزين إلى `{{target}}` — {{percent}}٪ ({{migrated}} / {{total}} كتلة). الرفع وإعادة التسمية والحذف والمشاركة مرفوضة؛ القراءة والتنزيل تعملان بشكل طبيعي.",
|
||||
"readonly_body": "الرفع وإعادة التسمية والحذف والمشاركة مرفوضة مؤقتًا. القراءة والتنزيل يعملان بشكل طبيعي."
|
||||
},
|
||||
"app": {
|
||||
"title": "OxiCloud",
|
||||
"description": "نظام تخزين سحابي بسيط"
|
||||
|
||||
@@ -38,6 +38,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"server_status": {
|
||||
"readonly_banner_aria": "Serverwartung läuft",
|
||||
"readonly_title": "Serverwartung läuft",
|
||||
"readonly_progress": "Speicher wird auf `{{target}}` migriert — {{percent}} % ({{migrated}} / {{total}} Blöcke). Uploads, Umbenennungen, Löschungen und Freigaben werden abgelehnt; Lesen und Herunterladen funktionieren normal.",
|
||||
"readonly_body": "Uploads, Umbenennungen, Löschungen und Freigaben werden vorübergehend abgelehnt. Lesen und Herunterladen funktionieren normal."
|
||||
},
|
||||
"app": {
|
||||
"title": "OxiCloud",
|
||||
"description": "Minimalistisches Cloud-Speichersystem"
|
||||
|
||||
@@ -38,6 +38,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"server_status": {
|
||||
"readonly_banner_aria": "Server maintenance in progress",
|
||||
"readonly_title": "Server maintenance in progress",
|
||||
"readonly_progress": "Migrating storage to `{{target}}` — {{percent}}% ({{migrated}} / {{total}} blobs). Uploads, renames, deletes, and shares are refused; reads and downloads work as normal.",
|
||||
"readonly_body": "Uploads, renames, deletes, and shares are refused temporarily. Reads and downloads work as normal."
|
||||
},
|
||||
"app": {
|
||||
"title": "OxiCloud",
|
||||
"description": "Minimalist cloud storage system"
|
||||
|
||||
@@ -38,6 +38,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"server_status": {
|
||||
"readonly_banner_aria": "Mantenimiento del servidor en curso",
|
||||
"readonly_title": "Mantenimiento del servidor en curso",
|
||||
"readonly_progress": "Migrando el almacenamiento a `{{target}}` — {{percent}} % ({{migrated}} / {{total}} bloques). Las subidas, renombres, eliminaciones y comparticiones se rechazan; las lecturas y descargas funcionan con normalidad.",
|
||||
"readonly_body": "Las subidas, renombres, eliminaciones y comparticiones se rechazan temporalmente. Las lecturas y descargas funcionan con normalidad."
|
||||
},
|
||||
"app": {
|
||||
"title": "OxiCloud",
|
||||
"description": "Sistema de almacenamiento en la nube minimalista"
|
||||
|
||||
@@ -38,6 +38,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"server_status": {
|
||||
"readonly_banner_aria": "نگهداری سرور در حال انجام است",
|
||||
"readonly_title": "نگهداری سرور در حال انجام است",
|
||||
"readonly_progress": "در حال انتقال حافظه به `{{target}}` — {{percent}}٪ ({{migrated}} / {{total}} بلاک). آپلود، تغییر نام، حذف و اشتراکگذاری رد میشوند؛ خواندن و دانلود عادی کار میکنند.",
|
||||
"readonly_body": "آپلود، تغییر نام، حذف و اشتراکگذاری موقتاً رد میشوند. خواندن و دانلود عادی کار میکنند."
|
||||
},
|
||||
"app": {
|
||||
"title": "OxiCloud",
|
||||
"description": "سیستم ذخیرهسازی ابری سادهگرا"
|
||||
|
||||
@@ -38,6 +38,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"server_status": {
|
||||
"readonly_banner_aria": "Maintenance du serveur en cours",
|
||||
"readonly_title": "Maintenance du serveur en cours",
|
||||
"readonly_progress": "Migration du stockage vers `{{target}}` — {{percent}} % ({{migrated}} / {{total}} blocs). Les téléversements, renommages, suppressions et partages sont refusés ; la lecture et le téléchargement continuent normalement.",
|
||||
"readonly_body": "Les téléversements, renommages, suppressions et partages sont temporairement refusés. La lecture et le téléchargement fonctionnent normalement."
|
||||
},
|
||||
"app": {
|
||||
"title": "OxiCloud",
|
||||
"description": "Système de stockage cloud minimaliste"
|
||||
|
||||
@@ -38,6 +38,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"server_status": {
|
||||
"readonly_banner_aria": "सर्वर रखरखाव प्रगति पर है",
|
||||
"readonly_title": "सर्वर रखरखाव प्रगति पर है",
|
||||
"readonly_progress": "स्टोरेज को `{{target}}` पर माइग्रेट किया जा रहा है — {{percent}}% ({{migrated}} / {{total}} ब्लॉब्स)। अपलोड, नाम बदलना, हटाना और साझा करना अस्वीकृत हैं; पढ़ना और डाउनलोड सामान्य रूप से काम करते हैं।",
|
||||
"readonly_body": "अपलोड, नाम बदलना, हटाना और साझा करना अस्थायी रूप से अस्वीकृत हैं। पढ़ना और डाउनलोड सामान्य रूप से काम करते हैं।"
|
||||
},
|
||||
"app": {
|
||||
"title": "OxiCloud",
|
||||
"description": "न्यूनतम क्लाउड स्टोरेज सिस्टम"
|
||||
|
||||
@@ -38,6 +38,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"server_status": {
|
||||
"readonly_banner_aria": "Manutenzione del server in corso",
|
||||
"readonly_title": "Manutenzione del server in corso",
|
||||
"readonly_progress": "Migrazione dello storage verso `{{target}}` — {{percent}}% ({{migrated}} / {{total}} blob). Caricamenti, rinomine, eliminazioni e condivisioni sono rifiutati; le letture e i download funzionano normalmente.",
|
||||
"readonly_body": "Caricamenti, rinomine, eliminazioni e condivisioni sono temporaneamente rifiutati. Le letture e i download funzionano normalmente."
|
||||
},
|
||||
"app": {
|
||||
"title": "OxiCloud",
|
||||
"description": "Sistema di archiviazione cloud minimalista"
|
||||
|
||||
@@ -38,6 +38,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"server_status": {
|
||||
"readonly_banner_aria": "サーバーメンテナンス中",
|
||||
"readonly_title": "サーバーメンテナンス中",
|
||||
"readonly_progress": "ストレージを `{{target}}` に移行中 — {{percent}}%({{migrated}} / {{total}} ブロブ)。アップロード、名前変更、削除、共有は拒否されます。読み取りとダウンロードは通常どおり動作します。",
|
||||
"readonly_body": "アップロード、名前変更、削除、共有は一時的に拒否されます。読み取りとダウンロードは通常どおり動作します。"
|
||||
},
|
||||
"app": {
|
||||
"title": "OxiCloud",
|
||||
"description": "ミニマリストクラウドストレージシステム"
|
||||
|
||||
@@ -38,6 +38,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"server_status": {
|
||||
"readonly_banner_aria": "서버 유지 관리 진행 중",
|
||||
"readonly_title": "서버 유지 관리 진행 중",
|
||||
"readonly_progress": "저장소를 `{{target}}`(으)로 마이그레이션 중 — {{percent}}% ({{migrated}} / {{total}} 블롭). 업로드, 이름 변경, 삭제 및 공유가 거부됩니다. 읽기 및 다운로드는 정상적으로 작동합니다.",
|
||||
"readonly_body": "업로드, 이름 변경, 삭제 및 공유가 일시적으로 거부됩니다. 읽기 및 다운로드는 정상적으로 작동합니다."
|
||||
},
|
||||
"app": {
|
||||
"title": "OxiCloud",
|
||||
"description": "미니멀리스트 클라우드 스토리지 시스템"
|
||||
|
||||
@@ -38,6 +38,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"server_status": {
|
||||
"readonly_banner_aria": "Serveronderhoud bezig",
|
||||
"readonly_title": "Serveronderhoud bezig",
|
||||
"readonly_progress": "Opslag wordt gemigreerd naar `{{target}}` — {{percent}}% ({{migrated}} / {{total}} blobs). Uploads, hernoemingen, verwijderingen en delen worden geweigerd; lezen en downloaden werken normaal.",
|
||||
"readonly_body": "Uploads, hernoemingen, verwijderingen en delen worden tijdelijk geweigerd. Lezen en downloaden werken normaal."
|
||||
},
|
||||
"app": {
|
||||
"title": "OxiCloud",
|
||||
"description": "Minimalistisch cloudopslagsysteem"
|
||||
|
||||
@@ -38,6 +38,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"server_status": {
|
||||
"readonly_banner_aria": "Trwa konserwacja serwera",
|
||||
"readonly_title": "Trwa konserwacja serwera",
|
||||
"readonly_progress": "Migracja pamięci do `{{target}}` — {{percent}}% ({{migrated}} / {{total}} blobów). Przesyłanie, zmiana nazwy, usuwanie i udostępnianie są odrzucane; odczyt i pobieranie działają normalnie.",
|
||||
"readonly_body": "Przesyłanie, zmiana nazwy, usuwanie i udostępnianie są tymczasowo odrzucane. Odczyt i pobieranie działają normalnie."
|
||||
},
|
||||
"app": {
|
||||
"title": "OxiCloud",
|
||||
"description": "Minimalistyczny cloud storage"
|
||||
|
||||
@@ -38,6 +38,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"server_status": {
|
||||
"readonly_banner_aria": "Manutenção do servidor em curso",
|
||||
"readonly_title": "Manutenção do servidor em curso",
|
||||
"readonly_progress": "A migrar o armazenamento para `{{target}}` — {{percent}}% ({{migrated}} / {{total}} blobs). Envios, renomeações, eliminações e partilhas são recusados; leituras e transferências funcionam normalmente.",
|
||||
"readonly_body": "Envios, renomeações, eliminações e partilhas são temporariamente recusados. Leituras e transferências funcionam normalmente."
|
||||
},
|
||||
"app": {
|
||||
"title": "OxiCloud",
|
||||
"description": "Sistema de armazenamento em nuvem minimalista"
|
||||
|
||||
@@ -38,6 +38,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"server_status": {
|
||||
"readonly_banner_aria": "Идёт обслуживание сервера",
|
||||
"readonly_title": "Идёт обслуживание сервера",
|
||||
"readonly_progress": "Миграция хранилища на `{{target}}` — {{percent}}% ({{migrated}} / {{total}} блобов). Загрузки, переименования, удаления и общий доступ отклоняются; чтение и скачивание работают как обычно.",
|
||||
"readonly_body": "Загрузки, переименования, удаления и общий доступ временно отклоняются. Чтение и скачивание работают как обычно."
|
||||
},
|
||||
"app": {
|
||||
"title": "OxiCloud",
|
||||
"description": "Минималистичная система облачного хранения"
|
||||
|
||||
@@ -38,6 +38,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"server_status": {
|
||||
"readonly_banner_aria": "伺服器維護進行中",
|
||||
"readonly_title": "伺服器維護進行中",
|
||||
"readonly_progress": "正在將儲存遷移至 `{{target}}` — {{percent}}%({{migrated}} / {{total}} 個 blob)。上傳、重新命名、刪除和分享會被拒絕;讀取和下載正常運作。",
|
||||
"readonly_body": "上傳、重新命名、刪除和分享暫時被拒絕。讀取和下載正常運作。"
|
||||
},
|
||||
"app": {
|
||||
"title": "OxiCloud",
|
||||
"description": "極簡雲端儲存系統"
|
||||
|
||||
@@ -38,6 +38,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"server_status": {
|
||||
"readonly_banner_aria": "服务器维护进行中",
|
||||
"readonly_title": "服务器维护进行中",
|
||||
"readonly_progress": "正在将存储迁移到 `{{target}}` — {{percent}}%({{migrated}} / {{total}} 个 blob)。上传、重命名、删除和共享被拒绝;读取和下载正常工作。",
|
||||
"readonly_body": "上传、重命名、删除和共享暂时被拒绝。读取和下载正常工作。"
|
||||
},
|
||||
"app": {
|
||||
"title": "OxiCloud",
|
||||
"description": "极简云存储系统"
|
||||
|
||||
Reference in New Issue
Block a user