mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
feat(storage): show unreferenced files in Admin > Storage, with the switch and a check
Desktop and phone show what the last check found (can be freed, unused for less than a week, freed), why a category was skipped, the switch that lets the nightly run delete and a report-only check on demand, through the shared storage panel hook. All 28 languages, and the Admin-Storage wiki page explains the check, the week's wait and when to leave it off.
This commit is contained in:
@@ -985,6 +985,7 @@
|
||||
"mobileOnly": {
|
||||
"src/mobile/screens/admin/MAdminResetMfaSheet.tsx": "Markup only; the 2FA reset confirm that MAdminSheets opens, with the reset handed in through props from its useAdminUserActions.",
|
||||
"src/mobile/screens/admin/MAdminUi.tsx": "Phone only admin UI primitives (button, card, field, input, secret input, sheet frame).",
|
||||
"src/mobile/screens/admin/MStorageReclaimCard.tsx": "Markup only; renders the reclaim view and handlers that MAdminStoragePanel takes from useStoragePanel, the twin of StorageReclaimSection.",
|
||||
"src/mobile/screens/atlas/MAtlasSearch.tsx": "Phone full screen country search overlay; options, suggestions and selection come through props from MAtlas.",
|
||||
"src/mobile/screens/atlas/MAtlasStatsCard.tsx": "Markup only; renders the AtlasStats that MAtlas passes in.",
|
||||
"src/mobile/screens/collections/MCollCategoryPicker.tsx": "Phone only category dropdown; value and categories come through props from the collection sheets.",
|
||||
|
||||
@@ -56,6 +56,8 @@ import {
|
||||
type BookingImportMode,
|
||||
type ReceiptScanResult,
|
||||
type StorageAdminState,
|
||||
type StorageReclaimReport,
|
||||
type StorageReclaimState,
|
||||
type StorageBackend,
|
||||
type StorageConfigPut,
|
||||
type StorageTestResponse,
|
||||
@@ -734,6 +736,12 @@ export const adminApi = {
|
||||
// A full scan of a large install can exceed the 8s instance timeout.
|
||||
refreshStorageStats: (): Promise<StorageUsage> =>
|
||||
apiClient.post('/admin/storage/stats/refresh', undefined, { timeout: 120_000 }).then(r => r.data),
|
||||
setStorageReclaimDelete: (deleteEnabled: boolean): Promise<StorageReclaimState> =>
|
||||
apiClient.put('/admin/storage/reclaim', { deleteEnabled }).then(r => r.data),
|
||||
// A check lists every reclaimable category and asks each domain about it; a
|
||||
// large bucket outlasts the 8s instance timeout. It only reports, whatever the switch says.
|
||||
runStorageReclaimCheck: (): Promise<StorageReclaimReport> =>
|
||||
apiClient.post('/admin/storage/reclaim/run', { reportOnly: true }, { timeout: 300_000 }).then(r => r.data),
|
||||
}
|
||||
|
||||
export const addonsApi = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { MASKED_SETTING_VALUE, type StorageAdminState, type StorageCategory, type StorageConfig } from '@trek/shared';
|
||||
import { MASKED_SETTING_VALUE, STORAGE_RECLAIM_CATEGORIES, type StorageAdminState, type StorageCategory, type StorageConfig, type StorageReclaimReport } from '@trek/shared';
|
||||
import { server } from '../../../../tests/helpers/msw/server';
|
||||
import { fireEvent, render, screen, waitFor, within } from '../../../../tests/helpers/render';
|
||||
import { ToastContainer } from '../../shared/Toast';
|
||||
@@ -11,6 +11,15 @@ const S3_MASKED = {
|
||||
secretAccessKey: MASKED_SETTING_VALUE, region: 'us-east-1', keyPrefix: '', retries: 1, timeoutMs: 30000,
|
||||
};
|
||||
|
||||
/** A last check with three covers a week old and two younger ones; every other category never reclaimed. */
|
||||
function reclaimReport(): StorageReclaimReport {
|
||||
const categories = Object.fromEntries(
|
||||
STORAGE_RECLAIM_CATEGORIES.map((c) => [c, { objects: 0, unreferenced: { objects: 0, bytes: 0 }, eligible: { objects: 0, bytes: 0 }, deleted: { objects: 0, bytes: 0 }, skipped: 'not_reclaimed' as const }]),
|
||||
) as StorageReclaimReport['categories'];
|
||||
categories.covers = { objects: 40, unreferenced: { objects: 5, bytes: 5000 }, eligible: { objects: 3, bytes: 3000 }, deleted: { objects: 0, bytes: 0 }, skipped: null };
|
||||
return { startedAt: Date.now() - 60_000, finishedAt: Date.now() - 30_000, mode: 'report', stopped: null, categories };
|
||||
}
|
||||
|
||||
function baseState(overrides: Partial<StorageAdminState> = {}): StorageAdminState {
|
||||
return {
|
||||
backends: [
|
||||
@@ -1218,4 +1227,28 @@ describe('AdminStoragePanel', () => {
|
||||
expect(within(row).getByText(/Sync failed: disk full/)).toBeInTheDocument();
|
||||
expect(within(row).getByRole('button', { name: 'Sync now' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-ADMIN-STOR-051: unreferenced files: the last check shows, the switch and the check go to the server', async () => {
|
||||
const lastRun = reclaimReport();
|
||||
let put: unknown = null;
|
||||
let run: unknown = null;
|
||||
server.use(
|
||||
http.put('/api/admin/storage/reclaim', async ({ request }) => {
|
||||
put = await request.json();
|
||||
return HttpResponse.json({ deleteEnabled: true, lastRun });
|
||||
}),
|
||||
http.post('/api/admin/storage/reclaim/run', async ({ request }) => {
|
||||
run = await request.json();
|
||||
return HttpResponse.json(lastRun);
|
||||
}),
|
||||
);
|
||||
await renderPanel(baseState({ reclaim: { deleteEnabled: false, lastRun } }));
|
||||
expect(screen.getByText('Unreferenced files')).toBeInTheDocument();
|
||||
expect(within(screen.getByTestId('storage-reclaim-covers')).getByText(/Can be freed/)).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Delete unreferenced files' }));
|
||||
await waitFor(() => expect(put).toEqual({ deleteEnabled: true }));
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: 'Delete unreferenced files' })).toHaveAttribute('aria-pressed', 'true'));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Check now' }));
|
||||
await waitFor(() => expect(run).toEqual({ reportOnly: true }));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
StatusPill,
|
||||
} from '../../Settings/settingsKit'
|
||||
import BackendForm from './BackendForm'
|
||||
import StorageReclaimSection from './StorageReclaimSection'
|
||||
import { CACHE_CATEGORIES, primaryNameOf } from './storageModel'
|
||||
import { categoryNames, migrationPromptLine, useStoragePanel } from './useStoragePanel'
|
||||
|
||||
@@ -96,6 +97,7 @@ export default function AdminStoragePanel(): React.ReactElement {
|
||||
const {
|
||||
admin, editing, setEditing, confirmRemove, setConfirmRemove, syncPrompt, setSyncPrompt, migratePrompt,
|
||||
closeMigratePrompt, migrationQueue, handleCancelMigration, handleRefreshStats, handleStartBackfill, handleCancelBackfill, loaded,
|
||||
reclaim, reclaimChecking, handleReclaimCheck, handleReclaimDelete,
|
||||
} = useStoragePanel()
|
||||
// Open/closed only: the render below and moveAndSave both recompute
|
||||
// computeMigrationCandidates(draft, state) fresh rather than reading the
|
||||
@@ -627,6 +629,15 @@ export default function AdminStoragePanel(): React.ReactElement {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-5">
|
||||
<StorageReclaimSection
|
||||
reclaim={reclaim}
|
||||
checking={reclaimChecking}
|
||||
onCheck={() => void handleReclaimCheck()}
|
||||
onDelete={(enabled) => void handleReclaimDelete(enabled)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{editing && (
|
||||
<BackendForm
|
||||
initial={editing.initial}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { Eraser, Loader2, ScanSearch } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from '../../../i18n';
|
||||
import { formatBytes } from '../../../utils/formatBytes';
|
||||
import { relativeTime } from '../../../utils/relativeTime';
|
||||
import Section from '../../Settings/Section';
|
||||
import ToggleSwitch from '../../Settings/ToggleSwitch';
|
||||
import { SETTINGS_BUTTON, SettingRow, SettingRows, SettingsHint, StatusPill } from '../../Settings/settingsKit';
|
||||
import { fs } from '../../shared/DialogShell';
|
||||
import type { ReclaimCount, ReclaimView } from './reclaimModel';
|
||||
|
||||
interface Props {
|
||||
reclaim: ReclaimView;
|
||||
checking: boolean;
|
||||
onCheck: () => void;
|
||||
onDelete: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unreferenced files on the desktop storage panel: what the last check found,
|
||||
* the switch that lets the nightly run delete them, and a check on demand. The
|
||||
* phone shows the same through MStorageReclaimCard, off the same view model.
|
||||
*/
|
||||
export default function StorageReclaimSection({ reclaim, checking, onCheck, onDelete }: Props): React.ReactElement {
|
||||
const { t, locale } = useTranslation();
|
||||
const count = (c: ReclaimCount) => t('storage.usage.line', { count: c.objects, size: formatBytes(c.bytes) });
|
||||
const checked = reclaim.checkedAt !== null;
|
||||
|
||||
return (
|
||||
<Section
|
||||
title={t('storage.reclaim.title')}
|
||||
icon={Eraser}
|
||||
hint={t('storage.reclaim.hint')}
|
||||
action={
|
||||
<button
|
||||
type="button"
|
||||
className={SETTINGS_BUTTON}
|
||||
style={fs(12.5, 'body')}
|
||||
onClick={onCheck}
|
||||
disabled={checking}
|
||||
>
|
||||
{checking ? (
|
||||
<Loader2 size={14} strokeWidth={2.2} className="animate-spin" />
|
||||
) : (
|
||||
<ScanSearch size={14} strokeWidth={2.2} />
|
||||
)}
|
||||
{t('storage.reclaim.check')}
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<SettingRows>
|
||||
<SettingRow
|
||||
label={t('storage.reclaim.delete')}
|
||||
hint={t('storage.reclaim.deleteHint')}
|
||||
control={
|
||||
<ToggleSwitch
|
||||
on={reclaim.deleteEnabled}
|
||||
onToggle={() => onDelete(!reclaim.deleteEnabled)}
|
||||
label={t('storage.reclaim.delete')}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<div className="px-3.5 py-3" data-testid="storage-reclaim-summary">
|
||||
<p className="m-0 text-content-faint" style={fs(11.5)}>
|
||||
{reclaim.checkedAt !== null
|
||||
? t('storage.reclaim.checked', { age: relativeTime(reclaim.checkedAt, locale) })
|
||||
: t('storage.reclaim.never')}
|
||||
</p>
|
||||
{reclaim.stopped && (
|
||||
<p role="note" className="m-0 mt-1.5 leading-snug text-warning" style={fs(11.5)}>
|
||||
{t('storage.reclaim.stopped')}
|
||||
</p>
|
||||
)}
|
||||
{checked && (
|
||||
<div className="mt-2.5 grid grid-cols-1 gap-2 sm:grid-cols-3">
|
||||
<Figure label={t('storage.reclaim.freeable')} value={count(reclaim.freeable)} />
|
||||
<Figure label={t('storage.reclaim.waiting')} value={count(reclaim.waiting)} />
|
||||
<Figure label={t('storage.reclaim.deleted')} value={count(reclaim.deleted)} />
|
||||
</div>
|
||||
)}
|
||||
{checked && reclaim.lines.length === 0 && (
|
||||
<SettingsHint className="mt-2.5">{t('storage.reclaim.nothing')}</SettingsHint>
|
||||
)}
|
||||
{reclaim.lines.length > 0 && (
|
||||
<ul className="m-0 mt-3 flex list-none flex-col gap-1.5 p-0">
|
||||
{reclaim.lines.map((line) => (
|
||||
<li
|
||||
key={line.category}
|
||||
data-testid={`storage-reclaim-${line.category}`}
|
||||
className="flex flex-wrap items-center gap-x-2.5 gap-y-1"
|
||||
>
|
||||
<span className="font-medium text-content-secondary" style={fs(12.5, 'body')}>
|
||||
{t(`storage.category.${line.category}`)}
|
||||
</span>
|
||||
{line.skipped ? (
|
||||
<StatusPill tone="warning">{t(`storage.reclaim.skip.${line.skipped}`)}</StatusPill>
|
||||
) : (
|
||||
<>
|
||||
{line.freeable.objects > 0 && (
|
||||
<span className="font-geist tabular-nums text-content-faint" style={fs(11.5)}>
|
||||
{t('storage.reclaim.freeable')}: {count(line.freeable)}
|
||||
</span>
|
||||
)}
|
||||
{line.waiting.objects > 0 && (
|
||||
<span className="font-geist tabular-nums text-content-faint" style={fs(11.5)}>
|
||||
{t('storage.reclaim.waiting')}: {count(line.waiting)}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</SettingRows>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
function Figure({ label, value }: { label: string; value: string }): React.ReactElement {
|
||||
return (
|
||||
<div className="min-w-0 rounded-[12px] bg-surface-secondary px-3 py-2">
|
||||
<p className="m-0 text-content-faint" style={fs(11)}>
|
||||
{label}
|
||||
</p>
|
||||
<p className="m-0 mt-0.5 font-geist font-semibold tabular-nums text-content" style={fs(12.5, 'body')}>
|
||||
{value}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// FE-ADMIN-STOR-RECLAIM-001 to -005: the reclaim report as both storage panels show it.
|
||||
import { STORAGE_RECLAIM_CATEGORIES, type StorageReclaimCategoryReport, type StorageReclaimReport } from '@trek/shared';
|
||||
|
||||
import { reclaimView } from './reclaimModel';
|
||||
|
||||
const none = { objects: 0, bytes: 0 };
|
||||
|
||||
function category(patch: Partial<StorageReclaimCategoryReport> = {}): StorageReclaimCategoryReport {
|
||||
return { objects: 0, unreferenced: none, eligible: none, deleted: none, skipped: null, ...patch };
|
||||
}
|
||||
|
||||
function report(
|
||||
patch: Partial<Record<(typeof STORAGE_RECLAIM_CATEGORIES)[number], StorageReclaimCategoryReport>>
|
||||
): StorageReclaimReport {
|
||||
const categories = Object.fromEntries(
|
||||
STORAGE_RECLAIM_CATEGORIES.map((c) => [c, category({ skipped: 'not_reclaimed' })])
|
||||
) as StorageReclaimReport['categories'];
|
||||
return {
|
||||
startedAt: 1_000,
|
||||
finishedAt: 2_000,
|
||||
mode: 'report',
|
||||
stopped: null,
|
||||
categories: { ...categories, ...patch },
|
||||
};
|
||||
}
|
||||
|
||||
describe('reclaimView', () => {
|
||||
it('FE-ADMIN-STOR-RECLAIM-001: no state reads as deletion off and never checked', () => {
|
||||
expect(reclaimView(null)).toEqual({
|
||||
deleteEnabled: false,
|
||||
checkedAt: null,
|
||||
stopped: false,
|
||||
freeable: none,
|
||||
waiting: none,
|
||||
deleted: none,
|
||||
lines: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('FE-ADMIN-STOR-RECLAIM-002: freeable is what is a week old, waiting what is younger, summed over categories', () => {
|
||||
const view = reclaimView({
|
||||
deleteEnabled: true,
|
||||
lastRun: report({
|
||||
covers: category({
|
||||
objects: 40,
|
||||
unreferenced: { objects: 5, bytes: 500 },
|
||||
eligible: { objects: 3, bytes: 300 },
|
||||
}),
|
||||
places: category({ objects: 10, unreferenced: { objects: 1, bytes: 100 } }),
|
||||
}),
|
||||
});
|
||||
expect(view.deleteEnabled).toBe(true);
|
||||
expect(view.checkedAt).toBe(2_000);
|
||||
expect(view.freeable).toEqual({ objects: 3, bytes: 300 });
|
||||
expect(view.waiting).toEqual({ objects: 3, bytes: 300 });
|
||||
expect(view.lines.map((line) => line.category)).toEqual(['covers', 'places']);
|
||||
});
|
||||
|
||||
it('FE-ADMIN-STOR-RECLAIM-003: what the run deleted is no longer freeable, and counts as freed', () => {
|
||||
const view = reclaimView({
|
||||
deleteEnabled: true,
|
||||
lastRun: report({
|
||||
files: category({
|
||||
unreferenced: { objects: 2, bytes: 20 },
|
||||
eligible: { objects: 2, bytes: 20 },
|
||||
deleted: { objects: 2, bytes: 20 },
|
||||
}),
|
||||
}),
|
||||
});
|
||||
expect(view.freeable).toEqual(none);
|
||||
expect(view.deleted).toEqual({ objects: 2, bytes: 20 });
|
||||
expect(view.lines).toEqual([]);
|
||||
});
|
||||
|
||||
it('FE-ADMIN-STOR-RECLAIM-004: a skipped category shows its reason and counts toward nothing, one never reclaimed shows nothing', () => {
|
||||
const view = reclaimView({
|
||||
deleteEnabled: false,
|
||||
lastRun: report({
|
||||
avatars: category({ skipped: 'too_many_unreferenced', unreferenced: { objects: 9, bytes: 9 } }),
|
||||
}),
|
||||
});
|
||||
expect(view.lines).toEqual([
|
||||
{ category: 'avatars', freeable: none, waiting: none, skipped: 'too_many_unreferenced' },
|
||||
]);
|
||||
expect(view.waiting).toEqual(none);
|
||||
expect(view.lines.some((line) => line.category === 'backups')).toBe(false);
|
||||
});
|
||||
|
||||
it('FE-ADMIN-STOR-RECLAIM-005: a check stopped by a restore says so', () => {
|
||||
expect(
|
||||
reclaimView({ deleteEnabled: false, lastRun: { ...report({}), stopped: 'database_reopened' } }).stopped
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import { STORAGE_RECLAIM_CATEGORIES, type StorageReclaimCategoryReport, type StorageReclaimState } from '@trek/shared';
|
||||
|
||||
/** A number of files and their size, as the report counts them. */
|
||||
export interface ReclaimCount {
|
||||
objects: number;
|
||||
bytes: number;
|
||||
}
|
||||
|
||||
/** What one category of the last check has to say, when it has anything to say. */
|
||||
interface ReclaimCategoryLine {
|
||||
category: (typeof STORAGE_RECLAIM_CATEGORIES)[number];
|
||||
/** Unused for a week or more: what the nightly run deletes once deletion is on. */
|
||||
freeable: ReclaimCount;
|
||||
/** Unused for less than a week: these get their week first. */
|
||||
waiting: ReclaimCount;
|
||||
/** Why the category was left alone; null when it was checked. */
|
||||
skipped: Exclude<StorageReclaimCategoryReport['skipped'], 'not_reclaimed'> | null;
|
||||
}
|
||||
|
||||
/** The admin's view of the reclaim pass: the switch and what the last check found. */
|
||||
export interface ReclaimView {
|
||||
deleteEnabled: boolean;
|
||||
/** When the last check finished, ms; null before the first one. */
|
||||
checkedAt: number | null;
|
||||
/** The last check stopped early, because a restore swapped the database under it. */
|
||||
stopped: boolean;
|
||||
freeable: ReclaimCount;
|
||||
waiting: ReclaimCount;
|
||||
/** What the last check deleted itself (only a nightly run with deletion on deletes). */
|
||||
deleted: ReclaimCount;
|
||||
/** Only the categories with something to free, something waiting, or a reason they were skipped. */
|
||||
lines: ReclaimCategoryLine[];
|
||||
}
|
||||
|
||||
const NONE: ReclaimCount = { objects: 0, bytes: 0 };
|
||||
|
||||
function add(a: ReclaimCount, b: ReclaimCount): ReclaimCount {
|
||||
return { objects: a.objects + b.objects, bytes: a.bytes + b.bytes };
|
||||
}
|
||||
|
||||
function minus(a: ReclaimCount, b: ReclaimCount): ReclaimCount {
|
||||
return { objects: Math.max(0, a.objects - b.objects), bytes: Math.max(0, a.bytes - b.bytes) };
|
||||
}
|
||||
|
||||
/**
|
||||
* The reclaim state as the storage panels show it, desktop and phone alike.
|
||||
* `null` (a server from before the reclaim pass, or a state not loaded) reads
|
||||
* as deletion off and never checked.
|
||||
*/
|
||||
export function reclaimView(state: StorageReclaimState | null): ReclaimView {
|
||||
const run = state?.lastRun ?? null;
|
||||
const lines: ReclaimCategoryLine[] = [];
|
||||
let freeable = NONE;
|
||||
let waiting = NONE;
|
||||
let deleted = NONE;
|
||||
for (const category of STORAGE_RECLAIM_CATEGORIES) {
|
||||
const report = run?.categories[category];
|
||||
if (!report || report.skipped === 'not_reclaimed') continue;
|
||||
// A skipped category frees nothing, whatever it counted: only its reason is shown.
|
||||
if (report.skipped !== null) {
|
||||
lines.push({ category, freeable: NONE, waiting: NONE, skipped: report.skipped });
|
||||
continue;
|
||||
}
|
||||
// What was deleted on this run no longer waits to be freed.
|
||||
const due = minus(report.eligible, report.deleted);
|
||||
const young = minus(report.unreferenced, report.eligible);
|
||||
freeable = add(freeable, due);
|
||||
waiting = add(waiting, young);
|
||||
deleted = add(deleted, report.deleted);
|
||||
if (due.objects > 0 || young.objects > 0) lines.push({ category, freeable: due, waiting: young, skipped: null });
|
||||
}
|
||||
return {
|
||||
deleteEnabled: state?.deleteEnabled ?? false,
|
||||
checkedAt: run?.finishedAt ?? null,
|
||||
stopped: run?.stopped != null,
|
||||
freeable,
|
||||
waiting,
|
||||
deleted,
|
||||
lines,
|
||||
};
|
||||
}
|
||||
@@ -58,6 +58,10 @@ export interface StorageAdmin {
|
||||
startMigration: (category: string, to: string) => Promise<string | null>
|
||||
cancelMigration: (category: string) => Promise<string | null>
|
||||
refreshStats: () => Promise<string | null>
|
||||
/** Switches the nightly deletion of unreferenced files on or off; null on success. */
|
||||
setReclaimDelete: (enabled: boolean) => Promise<string | null>
|
||||
/** Runs a check now that only reports; null on success. */
|
||||
checkReclaim: () => Promise<string | null>
|
||||
/** Re-GETs admin state (poll-safe — see the implementation comment below). Exposed for the panel's failed-queue recovery path. */
|
||||
refreshState: () => Promise<void>
|
||||
/**
|
||||
@@ -356,6 +360,34 @@ export function useStorageAdmin(genericError: string, conflictError: string): St
|
||||
}
|
||||
}, [genericError])
|
||||
|
||||
/** Writes the reclaim part of the state the way refreshStats writes usage. */
|
||||
const setReclaim = useCallback((reclaim: NonNullable<StorageAdminState['reclaim']>) => {
|
||||
const prev = stateRef.current
|
||||
if (!prev) return
|
||||
const next = { ...prev, reclaim }
|
||||
stateRef.current = next
|
||||
setState(next)
|
||||
}, [])
|
||||
|
||||
const setReclaimDelete = useCallback(async (enabled: boolean): Promise<string | null> => {
|
||||
try {
|
||||
setReclaim(await adminApi.setStorageReclaimDelete(enabled))
|
||||
return null
|
||||
} catch (err: unknown) {
|
||||
return getApiErrorMessage(err, genericError)
|
||||
}
|
||||
}, [genericError, setReclaim])
|
||||
|
||||
const checkReclaim = useCallback(async (): Promise<string | null> => {
|
||||
try {
|
||||
const lastRun = await adminApi.runStorageReclaimCheck()
|
||||
setReclaim({ deleteEnabled: stateRef.current?.reclaim?.deleteEnabled ?? false, lastRun })
|
||||
return null
|
||||
} catch (err: unknown) {
|
||||
return getApiErrorMessage(err, genericError)
|
||||
}
|
||||
}, [genericError, setReclaim])
|
||||
|
||||
return {
|
||||
state,
|
||||
storageBusy,
|
||||
@@ -376,6 +408,8 @@ export function useStorageAdmin(genericError: string, conflictError: string): St
|
||||
startMigration,
|
||||
cancelMigration,
|
||||
refreshStats,
|
||||
setReclaimDelete,
|
||||
checkReclaim,
|
||||
refreshState,
|
||||
discardDraft,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// FE-ADMIN-STOR-PANEL-001 to -017: the storage panel logic behind both admin shells.
|
||||
// FE-ADMIN-STOR-PANEL-001 to -019: the storage panel logic behind both admin shells.
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
|
||||
import { STORAGE_CATEGORIES, type StorageAdminState, type StorageBackend, type StorageConfig } from '@trek/shared';
|
||||
@@ -58,6 +58,8 @@ function makeAdmin(state: StorageAdminState | null, draft?: StorageConfig): Stor
|
||||
startMigration: vi.fn(async () => null),
|
||||
cancelMigration: vi.fn(async () => null),
|
||||
refreshStats: vi.fn(async () => null),
|
||||
setReclaimDelete: vi.fn(async () => null),
|
||||
checkReclaim: vi.fn(async () => null),
|
||||
refreshState: vi.fn(async () => {}),
|
||||
discardDraft: vi.fn(async () => {}),
|
||||
};
|
||||
@@ -283,3 +285,29 @@ describe('migrationPromptLine', () => {
|
||||
).toBe('storage.migrate.promptLineUnknown {"category":"storage.category.covers","from":"a","to":"b"}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useStoragePanel: unreferenced files', () => {
|
||||
it('FE-ADMIN-STOR-PANEL-018: the reclaim switch and the check go through the admin hook, a failure as a toast', async () => {
|
||||
fake.admin = makeAdmin(baseState());
|
||||
vi.mocked(fake.admin.checkReclaim).mockResolvedValueOnce('busy');
|
||||
const { result } = renderHook(() => useStoragePanel());
|
||||
await act(async () => {
|
||||
await result.current.handleReclaimDelete(true);
|
||||
});
|
||||
expect(fake.admin.setReclaimDelete).toHaveBeenCalledWith(true);
|
||||
await act(async () => {
|
||||
await result.current.handleReclaimCheck();
|
||||
});
|
||||
expect(fake.admin.checkReclaim).toHaveBeenCalledTimes(1);
|
||||
expect(toast.error).toHaveBeenCalledWith('busy');
|
||||
expect(result.current.reclaimChecking).toBe(false);
|
||||
});
|
||||
|
||||
it('FE-ADMIN-STOR-PANEL-019: a server without the reclaim state reads as deletion off and never checked', () => {
|
||||
fake.admin = makeAdmin(baseState());
|
||||
const { result } = renderHook(() => useStoragePanel());
|
||||
expect(result.current.reclaim.deleteEnabled).toBe(false);
|
||||
expect(result.current.reclaim.checkedAt).toBeNull();
|
||||
expect(result.current.reclaim.lines).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
import { useTranslation } from '../../../i18n';
|
||||
import { formatBytes } from '../../../utils/formatBytes';
|
||||
import { useToast } from '../../shared/Toast';
|
||||
import { reclaimView } from './reclaimModel';
|
||||
import {
|
||||
adoptedMirrorFor,
|
||||
computeMigrationCandidates,
|
||||
@@ -165,6 +166,19 @@ export function useStoragePanel() {
|
||||
if (error) toast.error(error);
|
||||
};
|
||||
|
||||
const [reclaimChecking, setReclaimChecking] = useState(false);
|
||||
const handleReclaimCheck = async () => {
|
||||
setReclaimChecking(true);
|
||||
const error = await admin.checkReclaim();
|
||||
setReclaimChecking(false);
|
||||
if (error) toast.error(error);
|
||||
};
|
||||
const handleReclaimDelete = async (enabled: boolean) => {
|
||||
const error = await admin.setReclaimDelete(enabled);
|
||||
if (error) toast.error(error);
|
||||
};
|
||||
const reclaim = reclaimView(admin.state?.reclaim ?? null);
|
||||
|
||||
const handleStartBackfill = async (row: FoldedBackendRow) => {
|
||||
if (!row.mirrorName) return;
|
||||
setSyncPrompt((prev) => (prev === row.name ? null : prev));
|
||||
@@ -371,6 +385,10 @@ export function useStoragePanel() {
|
||||
migrationQueue,
|
||||
handleCancelMigration,
|
||||
handleRefreshStats,
|
||||
reclaim,
|
||||
reclaimChecking,
|
||||
handleReclaimCheck,
|
||||
handleReclaimDelete,
|
||||
handleStartBackfill,
|
||||
handleCancelBackfill,
|
||||
loaded,
|
||||
|
||||
@@ -17,6 +17,7 @@ import MToggle from '../../components/MToggle'
|
||||
import MStorageTestResult from '../../components/MStorageTestResult'
|
||||
import MSetPickerSheet from '../settings/MSetPickerSheet'
|
||||
import MConfirmSheet from '../settings/MConfirmSheet'
|
||||
import MStorageReclaimCard from './MStorageReclaimCard'
|
||||
import { MSetSelectRow } from '../settings/MSettingsUi'
|
||||
import { MAdminButton, MAdminCard, MAdminCardHead, MAdminField, MAdminInput, MAdminSecretInput } from './MAdminUi'
|
||||
|
||||
@@ -172,6 +173,7 @@ export default function MAdminStoragePanel(): React.ReactElement {
|
||||
const {
|
||||
admin, editing, setEditing, confirmRemove, setConfirmRemove, syncPrompt, setSyncPrompt, migratePrompt,
|
||||
handleCancelMigration, handleRefreshStats, handleStartBackfill, handleCancelBackfill, loaded,
|
||||
reclaim, reclaimChecking, handleReclaimCheck, handleReclaimDelete,
|
||||
} = useStoragePanel()
|
||||
const [categoryPicker, setCategoryPicker] = useState<StorageCategory | null>(null)
|
||||
|
||||
@@ -565,6 +567,13 @@ export default function MAdminStoragePanel(): React.ReactElement {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<MStorageReclaimCard
|
||||
reclaim={reclaim}
|
||||
checking={reclaimChecking}
|
||||
onCheck={() => void handleReclaimCheck()}
|
||||
onDelete={(enabled) => void handleReclaimDelete(enabled)}
|
||||
/>
|
||||
|
||||
<MConfirmSheet
|
||||
open={confirmRemove !== null}
|
||||
onClose={() => setConfirmRemove(null)}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import React from 'react';
|
||||
import type { ReclaimCount, ReclaimView } from '../../../components/Admin/storage/reclaimModel';
|
||||
import { useTranslation } from '../../../i18n';
|
||||
import { formatBytes } from '../../../utils/formatBytes';
|
||||
import { relativeTime } from '../../../utils/relativeTime';
|
||||
import MToggle from '../../components/MToggle';
|
||||
import { MAdminButton, MAdminCard, MAdminCardHead, MAdminRow } from './MAdminUi';
|
||||
|
||||
interface Props {
|
||||
reclaim: ReclaimView;
|
||||
checking: boolean;
|
||||
onCheck: () => void;
|
||||
onDelete: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
/** Unreferenced files on the phone's storage panel, off the view model StorageReclaimSection shows on desktop. */
|
||||
export default function MStorageReclaimCard({ reclaim, checking, onCheck, onDelete }: Props): React.ReactElement {
|
||||
const { t, locale } = useTranslation();
|
||||
const count = (c: ReclaimCount) => t('storage.usage.line', { count: c.objects, size: formatBytes(c.bytes) });
|
||||
const checked = reclaim.checkedAt !== null;
|
||||
|
||||
return (
|
||||
<MAdminCard>
|
||||
<MAdminCardHead
|
||||
title={t('storage.reclaim.title')}
|
||||
hint={t('storage.reclaim.hint')}
|
||||
trailing={
|
||||
<MAdminButton variant="ghost" busy={checking} onClick={onCheck}>
|
||||
{t('storage.reclaim.check')}
|
||||
</MAdminButton>
|
||||
}
|
||||
/>
|
||||
<MAdminRow
|
||||
first
|
||||
title={t('storage.reclaim.delete')}
|
||||
hint={t('storage.reclaim.deleteHint')}
|
||||
trailing={
|
||||
<MToggle checked={reclaim.deleteEnabled} onChange={onDelete} ariaLabel={t('storage.reclaim.delete')} />
|
||||
}
|
||||
/>
|
||||
<div className="border-t border-[color:var(--m-rowbr)] pt-[11px]" data-testid="m-storage-reclaim-summary">
|
||||
<p className="font-geist text-[0.625rem] text-m-muted">
|
||||
{reclaim.checkedAt !== null
|
||||
? t('storage.reclaim.checked', { age: relativeTime(reclaim.checkedAt, locale) })
|
||||
: t('storage.reclaim.never')}
|
||||
</p>
|
||||
{reclaim.stopped && (
|
||||
<p role="note" className="mt-1 font-geist text-[0.625rem] text-m-muted">
|
||||
{t('storage.reclaim.stopped')}
|
||||
</p>
|
||||
)}
|
||||
{checked && (
|
||||
<div className="mt-2 space-y-1">
|
||||
<Figure label={t('storage.reclaim.freeable')} value={count(reclaim.freeable)} />
|
||||
<Figure label={t('storage.reclaim.waiting')} value={count(reclaim.waiting)} />
|
||||
<Figure label={t('storage.reclaim.deleted')} value={count(reclaim.deleted)} />
|
||||
</div>
|
||||
)}
|
||||
{checked && reclaim.lines.length === 0 && (
|
||||
<p className="mt-2 font-geist text-[0.625rem] text-m-muted">{t('storage.reclaim.nothing')}</p>
|
||||
)}
|
||||
{reclaim.lines.length > 0 && (
|
||||
<div className="mt-2 space-y-1.5">
|
||||
{reclaim.lines.map((line) => (
|
||||
<div key={line.category} data-testid={`m-storage-reclaim-${line.category}`}>
|
||||
<p className="text-[0.75rem] font-bold text-m-ink">{t(`storage.category.${line.category}`)}</p>
|
||||
{line.skipped ? (
|
||||
<p className="font-geist text-[0.625rem] text-m-muted">{t(`storage.reclaim.skip.${line.skipped}`)}</p>
|
||||
) : (
|
||||
<>
|
||||
{line.freeable.objects > 0 && (
|
||||
<p className="font-geist text-[0.625rem] text-m-muted">
|
||||
{t('storage.reclaim.freeable')}: {count(line.freeable)}
|
||||
</p>
|
||||
)}
|
||||
{line.waiting.objects > 0 && (
|
||||
<p className="font-geist text-[0.625rem] text-m-muted">
|
||||
{t('storage.reclaim.waiting')}: {count(line.waiting)}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</MAdminCard>
|
||||
);
|
||||
}
|
||||
|
||||
function Figure({ label, value }: { label: string; value: string }): React.ReactElement {
|
||||
return (
|
||||
<div className="flex items-baseline justify-between gap-3">
|
||||
<span className="font-geist text-[0.6875rem] text-m-muted">{label}</span>
|
||||
<span className="font-geist text-[0.6875rem] font-bold tabular-nums text-m-ink">{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,20 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { MASKED_SETTING_VALUE, type StorageAdminState, type StorageCategory, type StorageConfig } from '@trek/shared';
|
||||
import { MASKED_SETTING_VALUE, STORAGE_RECLAIM_CATEGORIES, type StorageAdminState, type StorageCategory, type StorageConfig, type StorageReclaimReport } from '@trek/shared';
|
||||
import { server } from '../../../helpers/msw/server';
|
||||
import { fireEvent, render, screen, waitFor, within } from '../../../helpers/render';
|
||||
import { ToastContainer } from '../../../../src/components/shared/Toast';
|
||||
import MAdminStoragePanel from '../../../../src/mobile/screens/admin/MAdminStoragePanel';
|
||||
|
||||
/** A last check with three covers a week old and two younger ones; every other category never reclaimed. */
|
||||
function reclaimReport(): StorageReclaimReport {
|
||||
const categories = Object.fromEntries(
|
||||
STORAGE_RECLAIM_CATEGORIES.map((c) => [c, { objects: 0, unreferenced: { objects: 0, bytes: 0 }, eligible: { objects: 0, bytes: 0 }, deleted: { objects: 0, bytes: 0 }, skipped: 'not_reclaimed' as const }]),
|
||||
) as StorageReclaimReport['categories'];
|
||||
categories.covers = { objects: 40, unreferenced: { objects: 5, bytes: 5000 }, eligible: { objects: 3, bytes: 3000 }, deleted: { objects: 0, bytes: 0 }, skipped: null };
|
||||
return { startedAt: Date.now() - 60_000, finishedAt: Date.now() - 30_000, mode: 'report', stopped: null, categories };
|
||||
}
|
||||
|
||||
function baseState(overrides: Partial<StorageAdminState> = {}): StorageAdminState {
|
||||
return {
|
||||
backends: [
|
||||
@@ -464,4 +473,28 @@ describe('MAdminStoragePanel', () => {
|
||||
expect(posted.every((b) => b.type !== 'mirror')).toBe(true);
|
||||
expect(posted.find((b) => b.name === 'off-box')?.options.endpoint).toBe('http://edited.example:9000');
|
||||
});
|
||||
|
||||
it('FE-MOB-MSTOR-019: unreferenced files: the last check shows, the switch and the check go to the server', async () => {
|
||||
const lastRun = reclaimReport();
|
||||
let put: unknown = null;
|
||||
let run: unknown = null;
|
||||
server.use(
|
||||
http.put('/api/admin/storage/reclaim', async ({ request }) => {
|
||||
put = await request.json();
|
||||
return HttpResponse.json({ deleteEnabled: true, lastRun });
|
||||
}),
|
||||
http.post('/api/admin/storage/reclaim/run', async ({ request }) => {
|
||||
run = await request.json();
|
||||
return HttpResponse.json(lastRun);
|
||||
}),
|
||||
);
|
||||
await renderPanel(baseState({ reclaim: { deleteEnabled: false, lastRun } }));
|
||||
expect(screen.getByText('Unreferenced files')).toBeInTheDocument();
|
||||
expect(within(screen.getByTestId('m-storage-reclaim-covers')).getByText(/Can be freed/)).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('switch', { name: 'Delete unreferenced files' }));
|
||||
await waitFor(() => expect(put).toEqual({ deleteEnabled: true }));
|
||||
await waitFor(() => expect(screen.getByRole('switch', { name: 'Delete unreferenced files' })).toHaveAttribute('aria-checked', 'true'));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Check now' }));
|
||||
await waitFor(() => expect(run).toEqual({ reportOnly: true }));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -148,5 +148,25 @@ const storage: TranslationStrings = {
|
||||
'storage.migrate.promptCancel': 'إلغاء',
|
||||
'storage.migrate.queued': 'في قائمة الانتظار: {categories}',
|
||||
'storage.migrate.queueDropped': 'تعذّر بدء عملية النقل التالية — تم مسح قائمة الانتظار المتبقية: {categories}',
|
||||
'storage.reclaim.title': 'الملفات غير المرتبطة',
|
||||
'storage.reclaim.hint':
|
||||
'يبحث TREK كل ليلة عن الملفات المرفوعة التي لم تعد أي رحلة أو مكان أو يومية أو مجموعة أو مستخدم يستخدمها، مثلاً بعد حذف رحلة.',
|
||||
'storage.reclaim.check': 'افحص الآن',
|
||||
'storage.reclaim.checked': 'تم الفحص {age}',
|
||||
'storage.reclaim.never': 'لم يتم الفحص بعد',
|
||||
'storage.reclaim.delete': 'حذف الملفات غير المرتبطة',
|
||||
'storage.reclaim.deleteHint':
|
||||
'عند الإيقاف، يكتفي الفحص الليلي بالإبلاغ. عند التفعيل، يُحذف أي ملف لم يُستخدم منذ أسبوع. أبقِه متوقفاً إذا كانت عدة تثبيتات تشترك في حاوية (Bucket) تخزين واحدة.',
|
||||
'storage.reclaim.freeable': 'يمكن تحرير مساحته',
|
||||
'storage.reclaim.waiting': 'غير مستخدمة منذ أقل من أسبوع',
|
||||
'storage.reclaim.deleted': 'تم تحريرها في آخر فحص',
|
||||
'storage.reclaim.nothing': 'لم يتم العثور على ملفات غير مرتبطة.',
|
||||
'storage.reclaim.stopped': 'توقف الفحص الأخير مبكراً لأن قاعدة البيانات استُعيدت أثناء تشغيله.',
|
||||
'storage.reclaim.skip.sources_missing': 'تم التخطي: تعذّر الاستعلام من كل أجزاء TREK',
|
||||
'storage.reclaim.skip.shared_location': 'تم التخطي: يشترك في موقعه مع فئة أخرى',
|
||||
'storage.reclaim.skip.too_many_unreferenced':
|
||||
'تم التخطي: يبدو أن عدداً كبيراً من الملفات غير مستخدم، ربما بسبب حاوية (Bucket) مشتركة',
|
||||
'storage.reclaim.skip.list_failed': 'تم التخطي: تعذّر سرد الملفات',
|
||||
'storage.reclaim.skip.source_failed': 'تم التخطي: فشل أحد الاستعلامات',
|
||||
};
|
||||
export default storage;
|
||||
|
||||
@@ -149,6 +149,26 @@ const storage: TranslationStrings = {
|
||||
'storage.migrate.promptCancel': 'Ləğv et',
|
||||
'storage.migrate.queued': 'Növbədədir: {categories}',
|
||||
'storage.migrate.queueDropped': 'Növbəti köçürməni başlatmaq mümkün olmadı — qalan növbə təmizləndi: {categories}',
|
||||
'storage.reclaim.title': 'İstinad olunmayan fayllar',
|
||||
'storage.reclaim.hint':
|
||||
'TREK hər gecə heç bir səfər, yer, jurnal, kolleksiya və ya istifadəçinin artıq istifadə etmədiyi yüklənmiş faylları axtarır, məsələn, səfər silindikdən sonra.',
|
||||
'storage.reclaim.check': 'İndi yoxla',
|
||||
'storage.reclaim.checked': 'Yoxlanıldı: {age}',
|
||||
'storage.reclaim.never': 'Hələ yoxlanılmayıb',
|
||||
'storage.reclaim.delete': 'İstinad olunmayan faylları sil',
|
||||
'storage.reclaim.deleteHint':
|
||||
'Söndürülü olduqda gecə yoxlaması yalnız hesabat verir. Aktiv olduqda bir həftədir heç nəyin istifadə etmədiyi fayl silinir. Bir neçə quraşdırma eyni bucket-i paylaşırsa, söndürülü saxlayın.',
|
||||
'storage.reclaim.freeable': 'Boşaldıla bilər',
|
||||
'storage.reclaim.waiting': 'Bir həftədən az istifadə olunmayıb',
|
||||
'storage.reclaim.deleted': 'Son yoxlamada boşaldıldı',
|
||||
'storage.reclaim.nothing': 'İstinad olunmayan fayl tapılmadı.',
|
||||
'storage.reclaim.stopped': 'Son yoxlama vaxtından əvvəl dayandı, çünki o işləyərkən verilənlər bazası bərpa olundu.',
|
||||
'storage.reclaim.skip.sources_missing': 'Keçildi: TREK-in bütün hissələrinə sorğu göndərmək mümkün olmadı',
|
||||
'storage.reclaim.skip.shared_location': 'Keçildi: yeri başqa kateqoriya ilə paylaşılır',
|
||||
'storage.reclaim.skip.too_many_unreferenced':
|
||||
'Keçildi: çox fayl istifadə olunmayan görünür, bəlkə paylaşılan bucket-dir',
|
||||
'storage.reclaim.skip.list_failed': 'Keçildi: faylların siyahısını almaq mümkün olmadı',
|
||||
'storage.reclaim.skip.source_failed': 'Keçildi: bir sorğu uğursuz oldu',
|
||||
};
|
||||
|
||||
export default storage;
|
||||
|
||||
@@ -138,5 +138,26 @@ const storage: TranslationStrings = {
|
||||
'storage.migrate.queued': 'Na fila: {categories}',
|
||||
'storage.migrate.queueDropped':
|
||||
'Não foi possível iniciar a próxima migração — a fila restante foi limpa: {categories}',
|
||||
'storage.reclaim.title': 'Arquivos sem referência',
|
||||
'storage.reclaim.hint':
|
||||
'Toda noite o TREK procura arquivos enviados que nenhuma viagem, lugar, diário, coleção ou usuário usa mais, por exemplo depois que uma viagem foi excluída.',
|
||||
'storage.reclaim.check': 'Verificar agora',
|
||||
'storage.reclaim.checked': 'Verificado {age}',
|
||||
'storage.reclaim.never': 'Ainda não verificado',
|
||||
'storage.reclaim.delete': 'Excluir arquivos sem referência',
|
||||
'storage.reclaim.deleteHint':
|
||||
'Desativado, a verificação noturna apenas informa. Ativado, um arquivo que ninguém usa há uma semana é excluído. Deixe desativado se várias instalações compartilham o mesmo bucket de armazenamento.',
|
||||
'storage.reclaim.freeable': 'Pode ser liberado',
|
||||
'storage.reclaim.waiting': 'Sem uso há menos de uma semana',
|
||||
'storage.reclaim.deleted': 'Liberado na última verificação',
|
||||
'storage.reclaim.nothing': 'Nenhum arquivo sem referência encontrado.',
|
||||
'storage.reclaim.stopped':
|
||||
'A última verificação parou antes do fim porque o banco de dados foi restaurado durante a execução.',
|
||||
'storage.reclaim.skip.sources_missing': 'Ignorado: nem todas as partes do TREK puderam ser consultadas',
|
||||
'storage.reclaim.skip.shared_location': 'Ignorado: compartilha o local com outra categoria',
|
||||
'storage.reclaim.skip.too_many_unreferenced':
|
||||
'Ignorado: muitos arquivos parecem sem uso, possivelmente um bucket compartilhado',
|
||||
'storage.reclaim.skip.list_failed': 'Ignorado: não foi possível listar os arquivos',
|
||||
'storage.reclaim.skip.source_failed': 'Ignorado: uma consulta falhou',
|
||||
};
|
||||
export default storage;
|
||||
|
||||
@@ -142,5 +142,25 @@ const storage: TranslationStrings = {
|
||||
'storage.migrate.queued': 'En cua: {categories}',
|
||||
'storage.migrate.queueDropped':
|
||||
"No s'ha pogut iniciar el trasllat següent — s'ha buidat la cua restant: {categories}",
|
||||
'storage.reclaim.title': 'Fitxers sense referència',
|
||||
'storage.reclaim.hint':
|
||||
"Cada nit TREK cerca fitxers pujats que cap viatge, lloc, diari, col·lecció o usuari fa servir ja, per exemple després d'eliminar un viatge.",
|
||||
'storage.reclaim.check': 'Comprova ara',
|
||||
'storage.reclaim.checked': 'Comprovat {age}',
|
||||
'storage.reclaim.never': 'Encara no comprovat',
|
||||
'storage.reclaim.delete': 'Elimina els fitxers sense referència',
|
||||
'storage.reclaim.deleteHint':
|
||||
"Desactivat, la comprovació nocturna només informa. Activat, s'elimina un fitxer que ningú no ha fet servir durant una setmana. Deixa-ho desactivat si diverses instal·lacions comparteixen un mateix bucket d'emmagatzematge.",
|
||||
'storage.reclaim.freeable': 'Es pot alliberar',
|
||||
'storage.reclaim.waiting': "Sense ús fa menys d'una setmana",
|
||||
'storage.reclaim.deleted': "Alliberat a l'última comprovació",
|
||||
'storage.reclaim.nothing': "No s'ha trobat cap fitxer sense referència.",
|
||||
'storage.reclaim.stopped':
|
||||
"L'última comprovació s'ha aturat abans d'hora perquè la base de dades es va restaurar mentre s'executava.",
|
||||
'storage.reclaim.skip.sources_missing': "Omès: no s'han pogut consultar totes les parts de TREK",
|
||||
'storage.reclaim.skip.shared_location': 'Omès: comparteix la ubicació amb una altra categoria',
|
||||
'storage.reclaim.skip.too_many_unreferenced': 'Omès: massa fitxers semblen sense ús, potser un bucket compartit',
|
||||
'storage.reclaim.skip.list_failed': "Omès: no s'han pogut llistar els fitxers",
|
||||
'storage.reclaim.skip.source_failed': 'Omès: ha fallat una consulta',
|
||||
};
|
||||
export default storage;
|
||||
|
||||
@@ -139,5 +139,25 @@ const storage: TranslationStrings = {
|
||||
'storage.migrate.promptCancel': 'Zrušit',
|
||||
'storage.migrate.queued': 'Ve frontě: {categories}',
|
||||
'storage.migrate.queueDropped': 'Nepodařilo se spustit další přesun — zbývající fronta byla vymazána: {categories}',
|
||||
'storage.reclaim.title': 'Nepoužívané soubory',
|
||||
'storage.reclaim.hint':
|
||||
'TREK každou noc hledá nahrané soubory, které už nepoužívá žádný výlet, místo, deník, sbírka ani uživatel, například po smazání výletu.',
|
||||
'storage.reclaim.check': 'Zkontrolovat nyní',
|
||||
'storage.reclaim.checked': 'Zkontrolováno {age}',
|
||||
'storage.reclaim.never': 'Zatím nezkontrolováno',
|
||||
'storage.reclaim.delete': 'Smazat nepoužívané soubory',
|
||||
'storage.reclaim.deleteHint':
|
||||
'Když je vypnuto, noční kontrola jen hlásí. Když je zapnuto, soubor, který se týden nepoužil, se smaže. Pokud několik instalací sdílí jeden bucket úložiště, nechte vypnuto.',
|
||||
'storage.reclaim.freeable': 'Lze uvolnit',
|
||||
'storage.reclaim.waiting': 'Nepoužito méně než týden',
|
||||
'storage.reclaim.deleted': 'Uvolněno při poslední kontrole',
|
||||
'storage.reclaim.nothing': 'Nebyly nalezeny žádné nepoužívané soubory.',
|
||||
'storage.reclaim.stopped': 'Poslední kontrola skončila předčasně, protože byla během jejího běhu obnovena databáze.',
|
||||
'storage.reclaim.skip.sources_missing': 'Přeskočeno: nešlo se zeptat všech částí TREK',
|
||||
'storage.reclaim.skip.shared_location': 'Přeskočeno: sdílí umístění s jinou kategorií',
|
||||
'storage.reclaim.skip.too_many_unreferenced':
|
||||
'Přeskočeno: příliš mnoho souborů vypadá nepoužitě, možná jde o sdílený bucket',
|
||||
'storage.reclaim.skip.list_failed': 'Přeskočeno: soubory se nepodařilo vypsat',
|
||||
'storage.reclaim.skip.source_failed': 'Přeskočeno: dotaz selhal',
|
||||
};
|
||||
export default storage;
|
||||
|
||||
@@ -142,5 +142,26 @@ const storage: TranslationStrings = {
|
||||
'storage.migrate.queued': 'In Warteschlange: {categories}',
|
||||
'storage.migrate.queueDropped':
|
||||
'Die nächste Verschiebung konnte nicht gestartet werden — die verbleibende Warteschlange wurde geleert: {categories}',
|
||||
'storage.reclaim.title': 'Nicht mehr genutzte Dateien',
|
||||
'storage.reclaim.hint':
|
||||
'Jede Nacht sucht TREK nach hochgeladenen Dateien, die keine Reise, kein Ort, kein Reisetagebuch, keine Sammlung und kein Nutzer mehr verwendet, etwa nach dem Löschen einer Reise.',
|
||||
'storage.reclaim.check': 'Jetzt prüfen',
|
||||
'storage.reclaim.checked': 'Geprüft {age}',
|
||||
'storage.reclaim.never': 'Noch nicht geprüft',
|
||||
'storage.reclaim.delete': 'Nicht mehr genutzte Dateien löschen',
|
||||
'storage.reclaim.deleteHint':
|
||||
'Aus: Die nächtliche Prüfung berichtet nur. An: Eine Datei, die eine Woche lang nichts mehr verwendet, wird gelöscht. Lass es aus, wenn sich mehrere Installationen einen Speicher-Bucket teilen.',
|
||||
'storage.reclaim.freeable': 'Kann freigegeben werden',
|
||||
'storage.reclaim.waiting': 'Seit weniger als einer Woche ungenutzt',
|
||||
'storage.reclaim.deleted': 'Bei der letzten Prüfung freigegeben',
|
||||
'storage.reclaim.nothing': 'Keine ungenutzten Dateien gefunden.',
|
||||
'storage.reclaim.stopped':
|
||||
'Die letzte Prüfung wurde vorzeitig beendet, weil währenddessen die Datenbank wiederhergestellt wurde.',
|
||||
'storage.reclaim.skip.sources_missing': 'Übersprungen: Nicht jeder Teil von TREK konnte gefragt werden',
|
||||
'storage.reclaim.skip.shared_location': 'Übersprungen: teilt den Speicherort mit einer anderen Kategorie',
|
||||
'storage.reclaim.skip.too_many_unreferenced':
|
||||
'Übersprungen: Zu viele Dateien wirken ungenutzt, womöglich ein geteilter Bucket',
|
||||
'storage.reclaim.skip.list_failed': 'Übersprungen: Die Dateien konnten nicht aufgelistet werden',
|
||||
'storage.reclaim.skip.source_failed': 'Übersprungen: Eine Abfrage ist fehlgeschlagen',
|
||||
};
|
||||
export default storage;
|
||||
|
||||
@@ -152,5 +152,24 @@ const storage: TranslationStrings = {
|
||||
'storage.migrate.promptCancel': 'Cancel',
|
||||
'storage.migrate.queued': 'Queued: {categories}',
|
||||
'storage.migrate.queueDropped': 'Could not start the next migration — the remaining queue was cleared: {categories}',
|
||||
'storage.reclaim.title': 'Unreferenced files',
|
||||
'storage.reclaim.hint':
|
||||
'Every night TREK looks for uploaded files that no trip, place, journal, collection or user uses any more, for example after a trip was deleted.',
|
||||
'storage.reclaim.check': 'Check now',
|
||||
'storage.reclaim.checked': 'Checked {age}',
|
||||
'storage.reclaim.never': 'Not checked yet',
|
||||
'storage.reclaim.delete': 'Delete unreferenced files',
|
||||
'storage.reclaim.deleteHint':
|
||||
'Off, the nightly check only reports. On, a file nothing has used for a week is deleted. Leave it off if several installations share one storage bucket.',
|
||||
'storage.reclaim.freeable': 'Can be freed',
|
||||
'storage.reclaim.waiting': 'Unused for less than a week',
|
||||
'storage.reclaim.deleted': 'Freed on the last check',
|
||||
'storage.reclaim.nothing': 'No unreferenced files found.',
|
||||
'storage.reclaim.stopped': 'The last check stopped early because the database was restored while it ran.',
|
||||
'storage.reclaim.skip.sources_missing': 'Skipped: not every part of TREK could be asked',
|
||||
'storage.reclaim.skip.shared_location': 'Skipped: shares its location with another category',
|
||||
'storage.reclaim.skip.too_many_unreferenced': 'Skipped: too many files look unused, possibly a shared bucket',
|
||||
'storage.reclaim.skip.list_failed': 'Skipped: the files could not be listed',
|
||||
'storage.reclaim.skip.source_failed': 'Skipped: a lookup failed',
|
||||
};
|
||||
export default storage;
|
||||
|
||||
@@ -142,5 +142,26 @@ const storage: TranslationStrings = {
|
||||
'storage.migrate.promptCancel': 'Cancelar',
|
||||
'storage.migrate.queued': 'En cola: {categories}',
|
||||
'storage.migrate.queueDropped': 'No se pudo iniciar la siguiente migración — se vació la cola restante: {categories}',
|
||||
'storage.reclaim.title': 'Archivos sin referencia',
|
||||
'storage.reclaim.hint':
|
||||
'Cada noche TREK busca archivos subidos que ningún viaje, lugar, diario, colección o usuario usa ya, por ejemplo tras eliminar un viaje.',
|
||||
'storage.reclaim.check': 'Comprobar ahora',
|
||||
'storage.reclaim.checked': 'Comprobado {age}',
|
||||
'storage.reclaim.never': 'Aún no comprobado',
|
||||
'storage.reclaim.delete': 'Eliminar archivos sin referencia',
|
||||
'storage.reclaim.deleteHint':
|
||||
'Desactivado, la comprobación nocturna solo informa. Activado, se elimina un archivo que nadie ha usado en una semana. Déjalo desactivado si varias instalaciones comparten un mismo bucket de almacenamiento.',
|
||||
'storage.reclaim.freeable': 'Se puede liberar',
|
||||
'storage.reclaim.waiting': 'Sin uso desde hace menos de una semana',
|
||||
'storage.reclaim.deleted': 'Liberado en la última comprobación',
|
||||
'storage.reclaim.nothing': 'No se encontraron archivos sin referencia.',
|
||||
'storage.reclaim.stopped':
|
||||
'La última comprobación se detuvo antes de tiempo porque la base de datos se restauró mientras se ejecutaba.',
|
||||
'storage.reclaim.skip.sources_missing': 'Omitido: no se pudo consultar a todas las partes de TREK',
|
||||
'storage.reclaim.skip.shared_location': 'Omitido: comparte ubicación con otra categoría',
|
||||
'storage.reclaim.skip.too_many_unreferenced':
|
||||
'Omitido: demasiados archivos parecen sin uso, posiblemente un bucket compartido',
|
||||
'storage.reclaim.skip.list_failed': 'Omitido: no se pudieron listar los archivos',
|
||||
'storage.reclaim.skip.source_failed': 'Omitido: falló una consulta',
|
||||
};
|
||||
export default storage;
|
||||
|
||||
@@ -156,5 +156,25 @@ const storage: TranslationStrings = {
|
||||
'storage.migrate.queued': 'Järjekorras: {categories}',
|
||||
'storage.migrate.queueDropped':
|
||||
'Järgmist teisaldamist ei saanud alustada — ülejäänud järjekord tühjendati: {categories}',
|
||||
'storage.reclaim.title': 'Viiteta failid',
|
||||
'storage.reclaim.hint':
|
||||
'TREK otsib igal ööl üleslaaditud faile, mida ükski reis, koht, päevik, kogu ega kasutaja enam ei kasuta, näiteks pärast reisi kustutamist.',
|
||||
'storage.reclaim.check': 'Kontrolli kohe',
|
||||
'storage.reclaim.checked': 'Kontrollitud {age}',
|
||||
'storage.reclaim.never': 'Pole veel kontrollitud',
|
||||
'storage.reclaim.delete': 'Kustuta viiteta failid',
|
||||
'storage.reclaim.deleteHint':
|
||||
'Väljas teavitab öine kontroll ainult. Sees kustutatakse fail, mida pole nädala jooksul keegi kasutanud. Jäta välja, kui mitu paigaldust jagavad ühte salve (bucket).',
|
||||
'storage.reclaim.freeable': 'Vabastatav',
|
||||
'storage.reclaim.waiting': 'Kasutamata alla nädala',
|
||||
'storage.reclaim.deleted': 'Vabastatud viimasel kontrollil',
|
||||
'storage.reclaim.nothing': 'Viiteta faile ei leitud.',
|
||||
'storage.reclaim.stopped': 'Viimane kontroll katkes ennatlikult, sest andmebaas taastati selle töö ajal.',
|
||||
'storage.reclaim.skip.sources_missing': 'Vahele jäetud: kõigilt TREK-i osadelt ei saanud päringut teha',
|
||||
'storage.reclaim.skip.shared_location': 'Vahele jäetud: jagab asukohta teise kategooriaga',
|
||||
'storage.reclaim.skip.too_many_unreferenced':
|
||||
'Vahele jäetud: liiga palju faile paistab kasutamata, võib-olla jagatud salv',
|
||||
'storage.reclaim.skip.list_failed': 'Vahele jäetud: faile ei saanud loetleda',
|
||||
'storage.reclaim.skip.source_failed': 'Vahele jäetud: päring ebaõnnestus',
|
||||
};
|
||||
export default storage;
|
||||
|
||||
@@ -144,5 +144,26 @@ const storage: TranslationStrings = {
|
||||
'storage.migrate.queued': "En file d'attente : {categories}",
|
||||
'storage.migrate.queueDropped':
|
||||
"Impossible de démarrer le déplacement suivant — la file d'attente restante a été vidée : {categories}",
|
||||
'storage.reclaim.title': 'Fichiers non référencés',
|
||||
'storage.reclaim.hint':
|
||||
"Chaque nuit, TREK recherche les fichiers envoyés qu'aucun voyage, lieu, journal, collection ou utilisateur n'utilise plus, par exemple après la suppression d'un voyage.",
|
||||
'storage.reclaim.check': 'Vérifier maintenant',
|
||||
'storage.reclaim.checked': 'Vérifié {age}',
|
||||
'storage.reclaim.never': 'Pas encore vérifié',
|
||||
'storage.reclaim.delete': 'Supprimer les fichiers non référencés',
|
||||
'storage.reclaim.deleteHint':
|
||||
'Désactivé, la vérification nocturne se contente de signaler. Activé, un fichier inutilisé depuis une semaine est supprimé. Laisse désactivé si plusieurs installations partagent un même bucket de stockage.',
|
||||
'storage.reclaim.freeable': 'Peut être libéré',
|
||||
'storage.reclaim.waiting': "Inutilisé depuis moins d'une semaine",
|
||||
'storage.reclaim.deleted': 'Libéré lors de la dernière vérification',
|
||||
'storage.reclaim.nothing': 'Aucun fichier non référencé trouvé.',
|
||||
'storage.reclaim.stopped':
|
||||
"La dernière vérification s'est arrêtée prématurément, car la base de données a été restaurée pendant son exécution.",
|
||||
'storage.reclaim.skip.sources_missing': "Ignoré : toutes les parties de TREK n'ont pas pu être interrogées",
|
||||
'storage.reclaim.skip.shared_location': 'Ignoré : partage son emplacement avec une autre catégorie',
|
||||
'storage.reclaim.skip.too_many_unreferenced':
|
||||
'Ignoré : trop de fichiers semblent inutilisés, peut-être un bucket partagé',
|
||||
'storage.reclaim.skip.list_failed': "Ignoré : la liste des fichiers n'a pas pu être obtenue",
|
||||
'storage.reclaim.skip.source_failed': 'Ignoré : une recherche a échoué',
|
||||
};
|
||||
export default storage;
|
||||
|
||||
@@ -145,5 +145,26 @@ const storage: TranslationStrings = {
|
||||
'storage.migrate.queued': 'Σε αναμονή: {categories}',
|
||||
'storage.migrate.queueDropped':
|
||||
'Δεν ήταν δυνατή η έναρξη της επόμενης μετακίνησης — η υπόλοιπη ουρά διαγράφηκε: {categories}',
|
||||
'storage.reclaim.title': 'Αρχεία χωρίς αναφορά',
|
||||
'storage.reclaim.hint':
|
||||
'Κάθε νύχτα το TREK αναζητά ανεβασμένα αρχεία που δεν χρησιμοποιούνται πια από κανένα ταξίδι, μέρος, ημερολόγιο, συλλογή ή χρήστη, για παράδειγμα μετά τη διαγραφή ενός ταξιδιού.',
|
||||
'storage.reclaim.check': 'Έλεγχος τώρα',
|
||||
'storage.reclaim.checked': 'Έλεγχος {age}',
|
||||
'storage.reclaim.never': 'Δεν έχει ελεγχθεί ακόμα',
|
||||
'storage.reclaim.delete': 'Διαγραφή αρχείων χωρίς αναφορά',
|
||||
'storage.reclaim.deleteHint':
|
||||
'Όταν είναι ανενεργό, ο νυχτερινός έλεγχος μόνο αναφέρει. Όταν είναι ενεργό, διαγράφεται κάθε αρχείο που δεν έχει χρησιμοποιηθεί για μία εβδομάδα. Άφησέ το ανενεργό αν πολλές εγκαταστάσεις μοιράζονται το ίδιο bucket αποθήκευσης.',
|
||||
'storage.reclaim.freeable': 'Μπορεί να ελευθερωθεί',
|
||||
'storage.reclaim.waiting': 'Αχρησιμοποίητα για λιγότερο από μία εβδομάδα',
|
||||
'storage.reclaim.deleted': 'Ελευθερώθηκε στον τελευταίο έλεγχο',
|
||||
'storage.reclaim.nothing': 'Δεν βρέθηκαν αρχεία χωρίς αναφορά.',
|
||||
'storage.reclaim.stopped':
|
||||
'Ο τελευταίος έλεγχος σταμάτησε νωρίς επειδή η βάση δεδομένων επαναφέρθηκε κατά την εκτέλεσή του.',
|
||||
'storage.reclaim.skip.sources_missing': 'Παραλείφθηκε: δεν ήταν δυνατή η ερώτηση σε όλα τα μέρη του TREK',
|
||||
'storage.reclaim.skip.shared_location': 'Παραλείφθηκε: μοιράζεται τη θέση του με άλλη κατηγορία',
|
||||
'storage.reclaim.skip.too_many_unreferenced':
|
||||
'Παραλείφθηκε: πάρα πολλά αρχεία φαίνονται αχρησιμοποίητα, πιθανώς κοινόχρηστο bucket',
|
||||
'storage.reclaim.skip.list_failed': 'Παραλείφθηκε: δεν ήταν δυνατή η λίστα των αρχείων',
|
||||
'storage.reclaim.skip.source_failed': 'Παραλείφθηκε: μια αναζήτηση απέτυχε',
|
||||
};
|
||||
export default storage;
|
||||
|
||||
@@ -127,6 +127,25 @@ const storage: TranslationStrings = {
|
||||
'storage.migrate.promptCancel': 'ביטול',
|
||||
'storage.migrate.queued': 'בתור: {categories}',
|
||||
'storage.migrate.queueDropped': 'לא ניתן היה להתחיל את ההעברה הבאה — התור שנותר נוקה: {categories}',
|
||||
'storage.reclaim.title': 'קבצים ללא שימוש',
|
||||
'storage.reclaim.hint':
|
||||
'בכל לילה TREK מחפש קבצים שהועלו ושאף טיול, מקום, יומן, אוסף או משתמש כבר לא משתמשים בהם, למשל אחרי שטיול נמחק.',
|
||||
'storage.reclaim.check': 'בדיקה עכשיו',
|
||||
'storage.reclaim.checked': 'נבדק {age}',
|
||||
'storage.reclaim.never': 'טרם נבדק',
|
||||
'storage.reclaim.delete': 'מחיקת קבצים ללא שימוש',
|
||||
'storage.reclaim.deleteHint':
|
||||
'כשהאפשרות כבויה, הבדיקה הלילית רק מדווחת. כשהיא פועלת, קובץ שלא היה בשימוש במשך שבוע נמחק. השאירו אותה כבויה אם כמה התקנות חולקות אותו באקט אחסון.',
|
||||
'storage.reclaim.freeable': 'ניתן לפנות',
|
||||
'storage.reclaim.waiting': 'לא בשימוש פחות משבוע',
|
||||
'storage.reclaim.deleted': 'פונה בבדיקה האחרונה',
|
||||
'storage.reclaim.nothing': 'לא נמצאו קבצים ללא שימוש.',
|
||||
'storage.reclaim.stopped': 'הבדיקה האחרונה נעצרה מוקדם כי מסד הנתונים שוחזר בזמן שהיא רצה.',
|
||||
'storage.reclaim.skip.sources_missing': 'דולג: לא ניתן היה לשאול את כל חלקי TREK',
|
||||
'storage.reclaim.skip.shared_location': 'דולג: חולק את המיקום שלו עם קטגוריה אחרת',
|
||||
'storage.reclaim.skip.too_many_unreferenced': 'דולג: נראה שיותר מדי קבצים אינם בשימוש, ייתכן שזה באקט משותף',
|
||||
'storage.reclaim.skip.list_failed': 'דולג: לא ניתן היה לרשום את הקבצים',
|
||||
'storage.reclaim.skip.source_failed': 'דולג: חיפוש נכשל',
|
||||
};
|
||||
|
||||
export default storage;
|
||||
|
||||
@@ -143,5 +143,25 @@ const storage: TranslationStrings = {
|
||||
'storage.migrate.queued': 'Várólistán: {categories}',
|
||||
'storage.migrate.queueDropped':
|
||||
'A következő áthelyezés nem indítható el — a hátralévő várólista törlésre került: {categories}',
|
||||
'storage.reclaim.title': 'Hivatkozás nélküli fájlok',
|
||||
'storage.reclaim.hint':
|
||||
'A TREK minden éjjel megkeresi azokat a feltöltött fájlokat, amelyeket már egyetlen utazás, hely, napló, gyűjtemény vagy felhasználó sem használ, például egy utazás törlése után.',
|
||||
'storage.reclaim.check': 'Ellenőrzés most',
|
||||
'storage.reclaim.checked': 'Ellenőrizve: {age}',
|
||||
'storage.reclaim.never': 'Még nem lett ellenőrizve',
|
||||
'storage.reclaim.delete': 'Hivatkozás nélküli fájlok törlése',
|
||||
'storage.reclaim.deleteHint':
|
||||
'Kikapcsolva az éjszakai ellenőrzés csak jelent. Bekapcsolva törlődik minden fájl, amelyet egy hete senki nem használt. Hagyd kikapcsolva, ha több telepítés oszt meg egy tárhely-bucketet.',
|
||||
'storage.reclaim.freeable': 'Felszabadítható',
|
||||
'storage.reclaim.waiting': 'Kevesebb mint egy hete nem használt',
|
||||
'storage.reclaim.deleted': 'Felszabadítva az utolsó ellenőrzéskor',
|
||||
'storage.reclaim.nothing': 'Nem található hivatkozás nélküli fájl.',
|
||||
'storage.reclaim.stopped': 'Az utolsó ellenőrzés idő előtt leállt, mert futás közben visszaállították az adatbázist.',
|
||||
'storage.reclaim.skip.sources_missing': 'Kihagyva: a TREK nem minden része volt lekérdezhető',
|
||||
'storage.reclaim.skip.shared_location': 'Kihagyva: a helyét egy másik kategóriával osztja meg',
|
||||
'storage.reclaim.skip.too_many_unreferenced':
|
||||
'Kihagyva: túl sok fájl tűnik használatlannak, lehet, hogy közös bucketről van szó',
|
||||
'storage.reclaim.skip.list_failed': 'Kihagyva: a fájlokat nem sikerült listázni',
|
||||
'storage.reclaim.skip.source_failed': 'Kihagyva: egy lekérdezés sikertelen volt',
|
||||
};
|
||||
export default storage;
|
||||
|
||||
@@ -138,5 +138,26 @@ const storage: TranslationStrings = {
|
||||
'storage.migrate.queued': 'Dalam antrean: {categories}',
|
||||
'storage.migrate.queueDropped':
|
||||
'Tidak dapat memulai pemindahan berikutnya — antrean yang tersisa telah dihapus: {categories}',
|
||||
'storage.reclaim.title': 'File tanpa referensi',
|
||||
'storage.reclaim.hint':
|
||||
'Setiap malam TREK mencari file yang diunggah tetapi tidak lagi dipakai oleh perjalanan, tempat, jurnal, koleksi, atau pengguna mana pun, misalnya setelah sebuah perjalanan dihapus.',
|
||||
'storage.reclaim.check': 'Periksa sekarang',
|
||||
'storage.reclaim.checked': 'Diperiksa {age}',
|
||||
'storage.reclaim.never': 'Belum diperiksa',
|
||||
'storage.reclaim.delete': 'Hapus file tanpa referensi',
|
||||
'storage.reclaim.deleteHint':
|
||||
'Nonaktif, pemeriksaan malam hanya melaporkan. Aktif, file yang tidak dipakai selama seminggu akan dihapus. Biarkan nonaktif jika beberapa instalasi berbagi satu bucket penyimpanan.',
|
||||
'storage.reclaim.freeable': 'Dapat dibebaskan',
|
||||
'storage.reclaim.waiting': 'Tidak dipakai kurang dari seminggu',
|
||||
'storage.reclaim.deleted': 'Dibebaskan pada pemeriksaan terakhir',
|
||||
'storage.reclaim.nothing': 'Tidak ada file tanpa referensi.',
|
||||
'storage.reclaim.stopped':
|
||||
'Pemeriksaan terakhir berhenti lebih awal karena database dipulihkan saat pemeriksaan berjalan.',
|
||||
'storage.reclaim.skip.sources_missing': 'Dilewati: tidak semua bagian TREK dapat ditanyai',
|
||||
'storage.reclaim.skip.shared_location': 'Dilewati: lokasinya sama dengan kategori lain',
|
||||
'storage.reclaim.skip.too_many_unreferenced':
|
||||
'Dilewati: terlalu banyak file yang tampak tidak terpakai, mungkin bucket bersama',
|
||||
'storage.reclaim.skip.list_failed': 'Dilewati: daftar file tidak dapat diambil',
|
||||
'storage.reclaim.skip.source_failed': 'Dilewati: salah satu pencarian gagal',
|
||||
};
|
||||
export default storage;
|
||||
|
||||
@@ -143,5 +143,25 @@ const storage: TranslationStrings = {
|
||||
'storage.migrate.queued': 'In coda: {categories}',
|
||||
'storage.migrate.queueDropped':
|
||||
'Impossibile avviare lo spostamento successivo — la coda rimanente è stata svuotata: {categories}',
|
||||
'storage.reclaim.title': 'File non referenziati',
|
||||
'storage.reclaim.hint':
|
||||
"Ogni notte TREK cerca i file caricati che nessun viaggio, luogo, diario, raccolta o utente usa più, per esempio dopo l'eliminazione di un viaggio.",
|
||||
'storage.reclaim.check': 'Controlla ora',
|
||||
'storage.reclaim.checked': 'Controllato {age}',
|
||||
'storage.reclaim.never': 'Non ancora controllato',
|
||||
'storage.reclaim.delete': 'Elimina i file non referenziati',
|
||||
'storage.reclaim.deleteHint':
|
||||
'Disattivato, il controllo notturno si limita a segnalare. Attivato, un file inutilizzato da una settimana viene eliminato. Lascialo disattivato se più installazioni condividono lo stesso bucket di archiviazione.',
|
||||
'storage.reclaim.freeable': 'Può essere liberato',
|
||||
'storage.reclaim.waiting': 'Inutilizzati da meno di una settimana',
|
||||
'storage.reclaim.deleted': "Liberato durante l'ultimo controllo",
|
||||
'storage.reclaim.nothing': 'Nessun file non referenziato trovato.',
|
||||
'storage.reclaim.stopped':
|
||||
"L'ultimo controllo si è interrotto prima del previsto perché il database è stato ripristinato mentre era in corso.",
|
||||
'storage.reclaim.skip.sources_missing': 'Saltato: non è stato possibile interrogare tutte le parti di TREK',
|
||||
'storage.reclaim.skip.shared_location': "Saltato: condivide la posizione con un'altra categoria",
|
||||
'storage.reclaim.skip.too_many_unreferenced': 'Saltato: troppi file sembrano inutilizzati, forse un bucket condiviso',
|
||||
'storage.reclaim.skip.list_failed': 'Saltato: non è stato possibile elencare i file',
|
||||
'storage.reclaim.skip.source_failed': 'Saltato: una ricerca non è riuscita',
|
||||
};
|
||||
export default storage;
|
||||
|
||||
@@ -135,5 +135,25 @@ const storage: TranslationStrings = {
|
||||
'storage.migrate.promptCancel': 'キャンセル',
|
||||
'storage.migrate.queued': 'キュー待ち: {categories}',
|
||||
'storage.migrate.queueDropped': '次の移動を開始できませんでした — 残りのキューはクリアされました: {categories}',
|
||||
'storage.reclaim.title': '参照されていないファイル',
|
||||
'storage.reclaim.hint':
|
||||
'TREKは毎晩、旅行、場所、ジャーナル、コレクション、ユーザーのいずれからも使われなくなったアップロード済みファイルを探します。たとえば旅行を削除した後などです。',
|
||||
'storage.reclaim.check': '今すぐチェック',
|
||||
'storage.reclaim.checked': '{age}にチェック済み',
|
||||
'storage.reclaim.never': '未チェック',
|
||||
'storage.reclaim.delete': '参照されていないファイルを削除',
|
||||
'storage.reclaim.deleteHint':
|
||||
'オフの場合、夜間チェックは報告のみ行います。オンの場合、1週間使われていないファイルが削除されます。複数のインストールで1つのストレージバケットを共有している場合は、オフのままにしてください。',
|
||||
'storage.reclaim.freeable': '解放可能',
|
||||
'storage.reclaim.waiting': '未使用期間が1週間未満',
|
||||
'storage.reclaim.deleted': '前回のチェックで解放',
|
||||
'storage.reclaim.nothing': '参照されていないファイルは見つかりませんでした。',
|
||||
'storage.reclaim.stopped': '前回のチェックは、実行中にデータベースが復元されたため途中で停止しました。',
|
||||
'storage.reclaim.skip.sources_missing': 'スキップ: TREKの一部に問い合わせできませんでした',
|
||||
'storage.reclaim.skip.shared_location': 'スキップ: 場所が別のカテゴリと共有されています',
|
||||
'storage.reclaim.skip.too_many_unreferenced':
|
||||
'スキップ: 未使用に見えるファイルが多すぎます。共有バケットの可能性があります',
|
||||
'storage.reclaim.skip.list_failed': 'スキップ: ファイルを一覧取得できませんでした',
|
||||
'storage.reclaim.skip.source_failed': 'スキップ: 検索に失敗しました',
|
||||
};
|
||||
export default storage;
|
||||
|
||||
@@ -132,5 +132,25 @@ const storage: TranslationStrings = {
|
||||
'storage.migrate.promptCancel': '취소',
|
||||
'storage.migrate.queued': '대기 중: {categories}',
|
||||
'storage.migrate.queueDropped': '다음 이동을 시작할 수 없습니다 — 남은 대기열이 지워졌습니다: {categories}',
|
||||
'storage.reclaim.title': '참조되지 않는 파일',
|
||||
'storage.reclaim.hint':
|
||||
'TREK은 매일 밤 어떤 여행, 장소, 일지, 컬렉션, 사용자도 더 이상 사용하지 않는 업로드 파일을 찾습니다. 예를 들어 여행이 삭제된 뒤에 생기는 파일입니다.',
|
||||
'storage.reclaim.check': '지금 확인',
|
||||
'storage.reclaim.checked': '{age} 확인됨',
|
||||
'storage.reclaim.never': '아직 확인하지 않음',
|
||||
'storage.reclaim.delete': '참조되지 않는 파일 삭제',
|
||||
'storage.reclaim.deleteHint':
|
||||
'끄면 야간 점검은 보고만 합니다. 켜면 일주일 동안 아무도 사용하지 않은 파일이 삭제됩니다. 여러 설치가 하나의 스토리지 버킷을 공유한다면 끄세요.',
|
||||
'storage.reclaim.freeable': '확보 가능',
|
||||
'storage.reclaim.waiting': '일주일 미만 동안 사용 안 됨',
|
||||
'storage.reclaim.deleted': '지난 확인에서 확보됨',
|
||||
'storage.reclaim.nothing': '참조되지 않는 파일이 없습니다.',
|
||||
'storage.reclaim.stopped': '실행 중에 데이터베이스가 복원되어 마지막 확인이 중간에 중단되었습니다.',
|
||||
'storage.reclaim.skip.sources_missing': '건너뜀: TREK의 일부 구성 요소를 조회할 수 없었습니다',
|
||||
'storage.reclaim.skip.shared_location': '건너뜀: 다른 카테고리와 위치를 공유합니다',
|
||||
'storage.reclaim.skip.too_many_unreferenced':
|
||||
'건너뜀: 사용되지 않는 것으로 보이는 파일이 너무 많습니다. 공유 버킷일 수 있습니다',
|
||||
'storage.reclaim.skip.list_failed': '건너뜀: 파일 목록을 가져올 수 없었습니다',
|
||||
'storage.reclaim.skip.source_failed': '건너뜀: 조회에 실패했습니다',
|
||||
};
|
||||
export default storage;
|
||||
|
||||
@@ -142,5 +142,26 @@ const storage: TranslationStrings = {
|
||||
'storage.migrate.queued': 'In wachtrij: {categories}',
|
||||
'storage.migrate.queueDropped':
|
||||
'Kon de volgende verplaatsing niet starten — de resterende wachtrij is gewist: {categories}',
|
||||
'storage.reclaim.title': 'Niet-gekoppelde bestanden',
|
||||
'storage.reclaim.hint':
|
||||
'TREK zoekt elke nacht naar geüploade bestanden die door geen enkele reis, plaats, dagboek, collectie of gebruiker meer worden gebruikt, bijvoorbeeld nadat een reis is verwijderd.',
|
||||
'storage.reclaim.check': 'Nu controleren',
|
||||
'storage.reclaim.checked': 'Gecontroleerd {age}',
|
||||
'storage.reclaim.never': 'Nog niet gecontroleerd',
|
||||
'storage.reclaim.delete': 'Niet-gekoppelde bestanden verwijderen',
|
||||
'storage.reclaim.deleteHint':
|
||||
'Uit: de nachtelijke controle rapporteert alleen. Aan: een bestand dat een week lang nergens is gebruikt, wordt verwijderd. Laat dit uit als meerdere installaties één opslagbucket delen.',
|
||||
'storage.reclaim.freeable': 'Kan worden vrijgemaakt',
|
||||
'storage.reclaim.waiting': 'Minder dan een week ongebruikt',
|
||||
'storage.reclaim.deleted': 'Vrijgemaakt bij de laatste controle',
|
||||
'storage.reclaim.nothing': 'Geen niet-gekoppelde bestanden gevonden.',
|
||||
'storage.reclaim.stopped':
|
||||
'De laatste controle is vroegtijdig gestopt omdat de database tijdens het uitvoeren werd hersteld.',
|
||||
'storage.reclaim.skip.sources_missing': 'Overgeslagen: niet elk onderdeel van TREK kon worden bevraagd',
|
||||
'storage.reclaim.skip.shared_location': 'Overgeslagen: deelt zijn locatie met een andere categorie',
|
||||
'storage.reclaim.skip.too_many_unreferenced':
|
||||
'Overgeslagen: te veel bestanden lijken ongebruikt, mogelijk een gedeelde bucket',
|
||||
'storage.reclaim.skip.list_failed': 'Overgeslagen: de bestanden konden niet worden opgesomd',
|
||||
'storage.reclaim.skip.source_failed': 'Overgeslagen: een opzoeking is mislukt',
|
||||
};
|
||||
export default storage;
|
||||
|
||||
@@ -147,5 +147,26 @@ const storage: TranslationStrings = {
|
||||
'storage.migrate.queued': 'W kolejce: {categories}',
|
||||
'storage.migrate.queueDropped':
|
||||
'Nie udało się rozpocząć kolejnego przenoszenia — pozostała kolejka została wyczyszczona: {categories}',
|
||||
'storage.reclaim.title': 'Pliki bez powiązań',
|
||||
'storage.reclaim.hint':
|
||||
'Każdej nocy TREK szuka przesłanych plików, których nie używa już żadna podróż, miejsce, dziennik, kolekcja ani użytkownik, na przykład po usunięciu podróży.',
|
||||
'storage.reclaim.check': 'Sprawdź teraz',
|
||||
'storage.reclaim.checked': 'Sprawdzono {age}',
|
||||
'storage.reclaim.never': 'Jeszcze nie sprawdzono',
|
||||
'storage.reclaim.delete': 'Usuń pliki bez powiązań',
|
||||
'storage.reclaim.deleteHint':
|
||||
'Wyłączone: nocna kontrola tylko raportuje. Włączone: plik, którego nic nie używało przez tydzień, jest usuwany. Zostaw wyłączone, jeśli kilka instalacji współdzieli jeden bucket magazynu.',
|
||||
'storage.reclaim.freeable': 'Można zwolnić',
|
||||
'storage.reclaim.waiting': 'Nieużywane krócej niż tydzień',
|
||||
'storage.reclaim.deleted': 'Zwolniono przy ostatniej kontroli',
|
||||
'storage.reclaim.nothing': 'Nie znaleziono plików bez powiązań.',
|
||||
'storage.reclaim.stopped':
|
||||
'Ostatnia kontrola zakończyła się wcześniej, ponieważ w trakcie jej trwania przywrócono bazę danych.',
|
||||
'storage.reclaim.skip.sources_missing': 'Pominięto: nie dało się zapytać każdej części TREK',
|
||||
'storage.reclaim.skip.shared_location': 'Pominięto: współdzieli lokalizację z inną kategorią',
|
||||
'storage.reclaim.skip.too_many_unreferenced':
|
||||
'Pominięto: zbyt wiele plików wygląda na nieużywane, możliwe że to współdzielony bucket',
|
||||
'storage.reclaim.skip.list_failed': 'Pominięto: nie udało się wylistować plików',
|
||||
'storage.reclaim.skip.source_failed': 'Pominięto: wyszukiwanie nie powiodło się',
|
||||
};
|
||||
export default storage;
|
||||
|
||||
@@ -148,5 +148,26 @@ const storage: TranslationStrings = {
|
||||
'storage.migrate.promptCancel': 'Отмена',
|
||||
'storage.migrate.queued': 'В очереди: {categories}',
|
||||
'storage.migrate.queueDropped': 'Не удалось запустить следующий перенос — оставшаяся очередь очищена: {categories}',
|
||||
'storage.reclaim.title': 'Файлы без связей',
|
||||
'storage.reclaim.hint':
|
||||
'Каждую ночь TREK ищет загруженные файлы, которые больше не используются ни в одной поездке, месте, дневнике, коллекции или профиле пользователя, например после удаления поездки.',
|
||||
'storage.reclaim.check': 'Проверить сейчас',
|
||||
'storage.reclaim.checked': 'Проверено {age}',
|
||||
'storage.reclaim.never': 'Ещё не проверялось',
|
||||
'storage.reclaim.delete': 'Удалять файлы без связей',
|
||||
'storage.reclaim.deleteHint':
|
||||
'Выкл.: ночная проверка только сообщает о файлах. Вкл.: файл, который неделю нигде не использовался, удаляется. Оставьте выключенным, если несколько установок используют один бакет хранилища.',
|
||||
'storage.reclaim.freeable': 'Можно освободить',
|
||||
'storage.reclaim.waiting': 'Не используются менее недели',
|
||||
'storage.reclaim.deleted': 'Освобождено при последней проверке',
|
||||
'storage.reclaim.nothing': 'Файлов без связей не найдено.',
|
||||
'storage.reclaim.stopped':
|
||||
'Последняя проверка остановлена досрочно, потому что во время её выполнения была восстановлена база данных.',
|
||||
'storage.reclaim.skip.sources_missing': 'Пропущено: не удалось опросить все части TREK',
|
||||
'storage.reclaim.skip.shared_location': 'Пропущено: расположение совпадает с другой категорией',
|
||||
'storage.reclaim.skip.too_many_unreferenced':
|
||||
'Пропущено: слишком много файлов выглядят неиспользуемыми, возможно, это общий бакет',
|
||||
'storage.reclaim.skip.list_failed': 'Пропущено: не удалось получить список файлов',
|
||||
'storage.reclaim.skip.source_failed': 'Пропущено: запрос не выполнен',
|
||||
};
|
||||
export default storage;
|
||||
|
||||
@@ -155,5 +155,25 @@ const storage: TranslationStrings = {
|
||||
'storage.migrate.promptCancel': 'Zrušiť',
|
||||
'storage.migrate.queued': 'V poradí: {categories}',
|
||||
'storage.migrate.queueDropped': 'Nepodarilo sa spustiť ďalší presun — zvyšné poradie bolo vymazané: {categories}',
|
||||
'storage.reclaim.title': 'Nepoužívané súbory',
|
||||
'storage.reclaim.hint':
|
||||
'TREK každú noc hľadá nahrané súbory, ktoré už nepoužíva žiadna cesta, miesto, denník, kolekcia ani používateľ, napríklad po odstránení cesty.',
|
||||
'storage.reclaim.check': 'Skontrolovať teraz',
|
||||
'storage.reclaim.checked': 'Skontrolované {age}',
|
||||
'storage.reclaim.never': 'Ešte neskontrolované',
|
||||
'storage.reclaim.delete': 'Odstraňovať nepoužívané súbory',
|
||||
'storage.reclaim.deleteHint':
|
||||
'Vypnuté: nočná kontrola len hlási. Zapnuté: súbor, ktorý týždeň nikto nepoužil, sa odstráni. Nechajte vypnuté, ak viaceré inštalácie zdieľajú jeden bucket úložiska.',
|
||||
'storage.reclaim.freeable': 'Možno uvoľniť',
|
||||
'storage.reclaim.waiting': 'Nepoužité menej ako týždeň',
|
||||
'storage.reclaim.deleted': 'Uvoľnené pri poslednej kontrole',
|
||||
'storage.reclaim.nothing': 'Nenašli sa žiadne nepoužívané súbory.',
|
||||
'storage.reclaim.stopped': 'Posledná kontrola sa predčasne zastavila, pretože sa počas jej behu obnovila databáza.',
|
||||
'storage.reclaim.skip.sources_missing': 'Preskočené: nebolo možné sa opýtať každej časti TREK',
|
||||
'storage.reclaim.skip.shared_location': 'Preskočené: zdieľa umiestnenie s inou kategóriou',
|
||||
'storage.reclaim.skip.too_many_unreferenced':
|
||||
'Preskočené: príliš veľa súborov vyzerá ako nepoužívaných, možno ide o zdieľaný bucket',
|
||||
'storage.reclaim.skip.list_failed': 'Preskočené: súbory sa nepodarilo vypísať',
|
||||
'storage.reclaim.skip.source_failed': 'Preskočené: vyhľadávanie zlyhalo',
|
||||
};
|
||||
export default storage;
|
||||
|
||||
@@ -138,5 +138,26 @@ const storage: TranslationStrings = {
|
||||
'storage.migrate.promptCancel': 'Avbryt',
|
||||
'storage.migrate.queued': 'I kö: {categories}',
|
||||
'storage.migrate.queueDropped': 'Det gick inte att starta nästa flytt — den återstående kön rensades: {categories}',
|
||||
'storage.reclaim.title': 'Filer utan koppling',
|
||||
'storage.reclaim.hint':
|
||||
'Varje natt letar TREK efter uppladdade filer som ingen resa, plats, dagbok, samling eller användare längre använder, till exempel efter att en resa har raderats.',
|
||||
'storage.reclaim.check': 'Kontrollera nu',
|
||||
'storage.reclaim.checked': 'Kontrollerades {age}',
|
||||
'storage.reclaim.never': 'Inte kontrollerat ännu',
|
||||
'storage.reclaim.delete': 'Radera filer utan koppling',
|
||||
'storage.reclaim.deleteHint':
|
||||
'Av: nattkontrollen rapporterar bara. På: en fil som ingenting har använt på en vecka raderas. Låt den vara av om flera installationer delar en lagringsbucket.',
|
||||
'storage.reclaim.freeable': 'Kan frigöras',
|
||||
'storage.reclaim.waiting': 'Oanvända i mindre än en vecka',
|
||||
'storage.reclaim.deleted': 'Frigjordes vid senaste kontrollen',
|
||||
'storage.reclaim.nothing': 'Inga filer utan koppling hittades.',
|
||||
'storage.reclaim.stopped':
|
||||
'Den senaste kontrollen avbröts i förtid eftersom databasen återställdes medan den pågick.',
|
||||
'storage.reclaim.skip.sources_missing': 'Hoppades över: alla delar av TREK kunde inte tillfrågas',
|
||||
'storage.reclaim.skip.shared_location': 'Hoppades över: delar plats med en annan kategori',
|
||||
'storage.reclaim.skip.too_many_unreferenced':
|
||||
'Hoppades över: för många filer verkar oanvända, möjligen en delad bucket',
|
||||
'storage.reclaim.skip.list_failed': 'Hoppades över: filerna kunde inte listas',
|
||||
'storage.reclaim.skip.source_failed': 'Hoppades över: en uppslagning misslyckades',
|
||||
};
|
||||
export default storage;
|
||||
|
||||
@@ -150,5 +150,25 @@ const storage: TranslationStrings = {
|
||||
'storage.migrate.promptCancel': 'ยกเลิก',
|
||||
'storage.migrate.queued': 'อยู่ในคิว: {categories}',
|
||||
'storage.migrate.queueDropped': 'ไม่สามารถเริ่มการย้ายข้อมูลครั้งถัดไปได้ - ล้างคิวที่เหลือแล้ว: {categories}',
|
||||
'storage.reclaim.title': 'ไฟล์ที่ไม่มีการอ้างอิง',
|
||||
'storage.reclaim.hint':
|
||||
'ทุกคืน TREK จะค้นหาไฟล์ที่อัปโหลดซึ่งไม่มีทริป สถานที่ บันทึก คอลเลกชัน หรือผู้ใช้คนใดใช้งานอีกต่อไป เช่น หลังจากลบทริป',
|
||||
'storage.reclaim.check': 'ตรวจสอบตอนนี้',
|
||||
'storage.reclaim.checked': 'ตรวจสอบเมื่อ {age}',
|
||||
'storage.reclaim.never': 'ยังไม่เคยตรวจสอบ',
|
||||
'storage.reclaim.delete': 'ลบไฟล์ที่ไม่มีการอ้างอิง',
|
||||
'storage.reclaim.deleteHint':
|
||||
'ปิด: การตรวจสอบรายคืนจะรายงานเท่านั้น เปิด: ไฟล์ที่ไม่มีการใช้งานเป็นเวลาหนึ่งสัปดาห์จะถูกลบ ควรปิดไว้หากหลายการติดตั้งใช้ถังเก็บข้อมูลเดียวกัน',
|
||||
'storage.reclaim.freeable': 'ว่างได้',
|
||||
'storage.reclaim.waiting': 'ไม่ได้ใช้งานน้อยกว่าหนึ่งสัปดาห์',
|
||||
'storage.reclaim.deleted': 'ว่างแล้วในการตรวจสอบครั้งล่าสุด',
|
||||
'storage.reclaim.nothing': 'ไม่พบไฟล์ที่ไม่มีการอ้างอิง',
|
||||
'storage.reclaim.stopped': 'การตรวจสอบครั้งล่าสุดหยุดก่อนกำหนด เพราะมีการกู้คืนฐานข้อมูลระหว่างที่ทำงาน',
|
||||
'storage.reclaim.skip.sources_missing': 'ข้าม: ไม่สามารถสอบถามทุกส่วนของ TREK ได้',
|
||||
'storage.reclaim.skip.shared_location': 'ข้าม: ใช้ตำแหน่งที่ตั้งเดียวกับหมวดหมู่อื่น',
|
||||
'storage.reclaim.skip.too_many_unreferenced':
|
||||
'ข้าม: มีไฟล์ที่ดูเหมือนไม่ได้ใช้มากเกินไป อาจเป็นถังเก็บข้อมูลที่ใช้ร่วมกัน',
|
||||
'storage.reclaim.skip.list_failed': 'ข้าม: ไม่สามารถแสดงรายการไฟล์ได้',
|
||||
'storage.reclaim.skip.source_failed': 'ข้าม: การค้นหาล้มเหลว',
|
||||
};
|
||||
export default storage;
|
||||
|
||||
@@ -140,5 +140,25 @@ const storage: TranslationStrings = {
|
||||
'storage.migrate.promptCancel': 'İptal',
|
||||
'storage.migrate.queued': 'Sırada: {categories}',
|
||||
'storage.migrate.queueDropped': 'Sonraki taşıma başlatılamadı — kalan kuyruk temizlendi: {categories}',
|
||||
'storage.reclaim.title': 'Başvurulmayan dosyalar',
|
||||
'storage.reclaim.hint':
|
||||
'TREK her gece, artık hiçbir gezi, yer, günlük, koleksiyon veya kullanıcı tarafından kullanılmayan yüklenmiş dosyaları arar, örneğin bir gezi silindikten sonra.',
|
||||
'storage.reclaim.check': 'Şimdi kontrol et',
|
||||
'storage.reclaim.checked': 'Kontrol edildi: {age}',
|
||||
'storage.reclaim.never': 'Henüz kontrol edilmedi',
|
||||
'storage.reclaim.delete': 'Başvurulmayan dosyaları sil',
|
||||
'storage.reclaim.deleteHint':
|
||||
"Kapalıyken gece kontrolü yalnızca rapor verir. Açıkken bir hafta boyunca hiçbir yerde kullanılmayan dosya silinir. Birden fazla kurulum aynı depolama bucket'ını paylaşıyorsa kapalı bırakın.",
|
||||
'storage.reclaim.freeable': 'Boşaltılabilir',
|
||||
'storage.reclaim.waiting': 'Bir haftadan kısa süredir kullanılmıyor',
|
||||
'storage.reclaim.deleted': 'Son kontrolde boşaltıldı',
|
||||
'storage.reclaim.nothing': 'Başvurulmayan dosya bulunamadı.',
|
||||
'storage.reclaim.stopped': 'Son kontrol erken durdu, çünkü çalışırken veritabanı geri yüklendi.',
|
||||
'storage.reclaim.skip.sources_missing': "Atlandı: TREK'in her bölümüne sorulamadı",
|
||||
'storage.reclaim.skip.shared_location': 'Atlandı: konumunu başka bir kategoriyle paylaşıyor',
|
||||
'storage.reclaim.skip.too_many_unreferenced':
|
||||
'Atlandı: çok fazla dosya kullanılmıyor gibi görünüyor, olasılıkla paylaşılan bir bucket',
|
||||
'storage.reclaim.skip.list_failed': 'Atlandı: dosyalar listelenemedi',
|
||||
'storage.reclaim.skip.source_failed': 'Atlandı: bir sorgu başarısız oldu',
|
||||
};
|
||||
export default storage;
|
||||
|
||||
@@ -147,5 +147,25 @@ const storage: TranslationStrings = {
|
||||
'storage.migrate.promptCancel': 'Скасувати',
|
||||
'storage.migrate.queued': 'У черзі: {categories}',
|
||||
'storage.migrate.queueDropped': 'Не вдалося розпочати наступне перенесення — залишок черги очищено: {categories}',
|
||||
'storage.reclaim.title': 'Файли без посилань',
|
||||
'storage.reclaim.hint':
|
||||
'Щоночі TREK шукає завантажені файли, які більше не використовує жодна поїздка, місце, щоденник, колекція чи користувач, наприклад після видалення поїздки.',
|
||||
'storage.reclaim.check': 'Перевірити зараз',
|
||||
'storage.reclaim.checked': 'Перевірено {age}',
|
||||
'storage.reclaim.never': 'Ще не перевірялося',
|
||||
'storage.reclaim.delete': 'Видаляти файли без посилань',
|
||||
'storage.reclaim.deleteHint':
|
||||
'Вимкнено: нічна перевірка лише повідомляє. Увімкнено: файл, який тиждень ніщо не використовує, видаляється. Залиште вимкненим, якщо кілька встановлень ділять один бакет сховища.',
|
||||
'storage.reclaim.freeable': 'Можна звільнити',
|
||||
'storage.reclaim.waiting': 'Не використовується менше тижня',
|
||||
'storage.reclaim.deleted': 'Звільнено під час останньої перевірки',
|
||||
'storage.reclaim.nothing': 'Файлів без посилань не знайдено.',
|
||||
'storage.reclaim.stopped': 'Остання перевірка зупинилася раніше, бо під час її роботи базу даних було відновлено.',
|
||||
'storage.reclaim.skip.sources_missing': 'Пропущено: не вдалося опитати всі частини TREK',
|
||||
'storage.reclaim.skip.shared_location': 'Пропущено: ділить розташування з іншою категорією',
|
||||
'storage.reclaim.skip.too_many_unreferenced':
|
||||
'Пропущено: забагато файлів здаються невикористаними, можливо, це спільний бакет',
|
||||
'storage.reclaim.skip.list_failed': 'Пропущено: не вдалося отримати список файлів',
|
||||
'storage.reclaim.skip.source_failed': 'Пропущено: пошук завершився помилкою',
|
||||
};
|
||||
export default storage;
|
||||
|
||||
@@ -136,5 +136,25 @@ const storage: TranslationStrings = {
|
||||
'storage.migrate.promptCancel': 'Hủy',
|
||||
'storage.migrate.queued': 'Đang chờ: {categories}',
|
||||
'storage.migrate.queueDropped': 'Không thể bắt đầu di chuyển tiếp theo — hàng đợi còn lại đã bị xóa: {categories}',
|
||||
'storage.reclaim.title': 'Tệp không còn được tham chiếu',
|
||||
'storage.reclaim.hint':
|
||||
'Mỗi đêm TREK tìm các tệp đã tải lên mà không chuyến đi, địa điểm, nhật ký, bộ sưu tập hay người dùng nào còn sử dụng, ví dụ sau khi một chuyến đi bị xóa.',
|
||||
'storage.reclaim.check': 'Kiểm tra ngay',
|
||||
'storage.reclaim.checked': 'Đã kiểm tra {age}',
|
||||
'storage.reclaim.never': 'Chưa kiểm tra',
|
||||
'storage.reclaim.delete': 'Xóa các tệp không còn được tham chiếu',
|
||||
'storage.reclaim.deleteHint':
|
||||
'Tắt: lần kiểm tra hằng đêm chỉ báo cáo. Bật: tệp không được dùng trong một tuần sẽ bị xóa. Hãy để tắt nếu nhiều bản cài đặt dùng chung một bucket lưu trữ.',
|
||||
'storage.reclaim.freeable': 'Có thể giải phóng',
|
||||
'storage.reclaim.waiting': 'Chưa dùng dưới một tuần',
|
||||
'storage.reclaim.deleted': 'Đã giải phóng ở lần kiểm tra trước',
|
||||
'storage.reclaim.nothing': 'Không tìm thấy tệp nào không còn được tham chiếu.',
|
||||
'storage.reclaim.stopped': 'Lần kiểm tra trước đã dừng sớm vì cơ sở dữ liệu được khôi phục trong khi đang chạy.',
|
||||
'storage.reclaim.skip.sources_missing': 'Bỏ qua: không thể truy vấn mọi phần của TREK',
|
||||
'storage.reclaim.skip.shared_location': 'Bỏ qua: dùng chung vị trí với một danh mục khác',
|
||||
'storage.reclaim.skip.too_many_unreferenced':
|
||||
'Bỏ qua: quá nhiều tệp có vẻ không được dùng, có thể là bucket dùng chung',
|
||||
'storage.reclaim.skip.list_failed': 'Bỏ qua: không thể liệt kê các tệp',
|
||||
'storage.reclaim.skip.source_failed': 'Bỏ qua: một truy vấn đã thất bại',
|
||||
};
|
||||
export default storage;
|
||||
|
||||
@@ -126,5 +126,24 @@ const storage: TranslationStrings = {
|
||||
'storage.migrate.promptCancel': '取消',
|
||||
'storage.migrate.queued': '排隊中:{categories}',
|
||||
'storage.migrate.queueDropped': '無法啟動下一個遷移 — 剩餘佇列已清空:{categories}',
|
||||
'storage.reclaim.title': '未被引用的檔案',
|
||||
'storage.reclaim.hint':
|
||||
'TREK 每晚會尋找不再被任何行程、地點、日誌、集合或使用者使用的已上傳檔案,例如行程被刪除之後。',
|
||||
'storage.reclaim.check': '立即檢查',
|
||||
'storage.reclaim.checked': '{age}檢查',
|
||||
'storage.reclaim.never': '尚未檢查',
|
||||
'storage.reclaim.delete': '刪除未被引用的檔案',
|
||||
'storage.reclaim.deleteHint':
|
||||
'關閉時,夜間檢查只會回報。開啟後,一週內無人使用的檔案會被刪除。如果多個安裝共用同一個儲存貯體,請保持關閉。',
|
||||
'storage.reclaim.freeable': '可釋放',
|
||||
'storage.reclaim.waiting': '未使用不足一週',
|
||||
'storage.reclaim.deleted': '上次檢查已釋放',
|
||||
'storage.reclaim.nothing': '未發現未被引用的檔案。',
|
||||
'storage.reclaim.stopped': '上次檢查提前停止,因為執行期間資料庫被還原了。',
|
||||
'storage.reclaim.skip.sources_missing': '已略過:無法查詢 TREK 的所有部分',
|
||||
'storage.reclaim.skip.shared_location': '已略過:與另一個類別共用位置',
|
||||
'storage.reclaim.skip.too_many_unreferenced': '已略過:看起來未使用的檔案過多,可能是共用儲存貯體',
|
||||
'storage.reclaim.skip.list_failed': '已略過:無法列出檔案',
|
||||
'storage.reclaim.skip.source_failed': '已略過:某次查詢失敗',
|
||||
};
|
||||
export default storage;
|
||||
|
||||
@@ -125,5 +125,23 @@ const storage: TranslationStrings = {
|
||||
'storage.migrate.promptCancel': '取消',
|
||||
'storage.migrate.queued': '排队中:{categories}',
|
||||
'storage.migrate.queueDropped': '无法启动下一个迁移 — 剩余队列已清空:{categories}',
|
||||
'storage.reclaim.title': '未被引用的文件',
|
||||
'storage.reclaim.hint': 'TREK 每晚会查找不再被任何行程、地点、日志、合集或用户使用的已上传文件,例如行程被删除之后。',
|
||||
'storage.reclaim.check': '立即检查',
|
||||
'storage.reclaim.checked': '{age}检查',
|
||||
'storage.reclaim.never': '尚未检查',
|
||||
'storage.reclaim.delete': '删除未被引用的文件',
|
||||
'storage.reclaim.deleteHint':
|
||||
'关闭时,夜间检查只做报告。开启后,一周内无人使用的文件会被删除。如果多个安装共用同一个存储桶,请保持关闭。',
|
||||
'storage.reclaim.freeable': '可释放',
|
||||
'storage.reclaim.waiting': '未使用不足一周',
|
||||
'storage.reclaim.deleted': '上次检查已释放',
|
||||
'storage.reclaim.nothing': '未发现未被引用的文件。',
|
||||
'storage.reclaim.stopped': '上次检查提前停止,因为运行期间数据库被恢复了。',
|
||||
'storage.reclaim.skip.sources_missing': '已跳过:无法查询 TREK 的所有部分',
|
||||
'storage.reclaim.skip.shared_location': '已跳过:与另一个类别共用位置',
|
||||
'storage.reclaim.skip.too_many_unreferenced': '已跳过:看起来未使用的文件过多,可能是共享存储桶',
|
||||
'storage.reclaim.skip.list_failed': '已跳过:无法列出文件',
|
||||
'storage.reclaim.skip.source_failed': '已跳过:某次查询失败',
|
||||
};
|
||||
export default storage;
|
||||
|
||||
@@ -125,6 +125,36 @@ stamp shows when the numbers were computed; reassigning categories does not
|
||||
retroactively move them until the next scan. The uploads backend's total
|
||||
includes the legacy photo library.
|
||||
|
||||
## Unreferenced files
|
||||
|
||||
Uploaded files can outlive what used them: deleting a trip, a user or a place
|
||||
image, or replacing a cover, leaves the old file in storage. Every night
|
||||
(04:05 server time) TREK checks the covers, place images, avatars, trip files
|
||||
and journal photos for files that no row names any more, and the
|
||||
**Unreferenced files** section shows what it found:
|
||||
|
||||
- **Can be freed**: unused for at least a week, so the nightly run deletes them
|
||||
once deletion is on.
|
||||
- **Unused for less than a week**: these get their week first. A file that is
|
||||
used again in the meantime (an undo, a copied trip) starts over.
|
||||
- **Freed on the last check**: what the nightly run deleted.
|
||||
|
||||
**Delete unreferenced files** is off after install and upgrade, so the check
|
||||
only reports. Switched on, the nightly run deletes a file once it has been
|
||||
unused for a week and a last check right before the delete still finds
|
||||
nothing that uses it. **Check now** runs a check that only reports, whatever
|
||||
the switch says. Each run is recorded in the [Audit-Log](Audit-Log).
|
||||
|
||||
A category is skipped, with the reason shown next to it, when not every part
|
||||
of TREK could be asked about it, when its files share a location with another
|
||||
category, when a listing or a lookup fails, or when more than a fifth of its
|
||||
files look unused. The last one usually means two installations share one
|
||||
bucket: each would see the other's files as unused, so leave deletion off in
|
||||
that setup. Caches (place photos), backups, assistant attachments and journal
|
||||
thumbnails are cleaned up by their own routines and are never touched here.
|
||||
No check runs on a demo instance, and none while a sync, a move or a restore
|
||||
is running.
|
||||
|
||||
## Secrets and encryption
|
||||
|
||||
Backend credentials (the S3 secret access key) are stored encrypted. Setting
|
||||
|
||||
Reference in New Issue
Block a user