feat(drive): add readonly policy

permmit admin to freeze a drive, trash janitor background job is also disabled for this drive
This commit is contained in:
Edouard Vanbelle
2026-07-15 22:21:15 +02:00
parent 346e2e879c
commit a6427fc028
33 changed files with 1094 additions and 95 deletions
+17
View File
@@ -39,6 +39,15 @@ export interface FolderItem {
parent_id: string | null;
path: string;
etag: string;
/**
* The drive this folder belongs to (post-D0 ownership pivot per
* `docs/plan/drive.md` §3). Populated by the backend `FolderDto`
* on every response; the field was left out of the TS type until
* a caller needed it. Used by `/files` to resolve the current
* drive for the read-only banner without depending on the URL's
* leading segment being a drive-root folder id.
*/
drive_id: string;
}
export interface FileItem {
@@ -307,6 +316,14 @@ export interface DrivePolicies {
* Symmetric shape to `include_in_photo_index`.
*/
include_in_music_index: boolean;
/**
* Full freeze / legal-hold. When `true`, every mutation on resources
* in the drive is refused — user-initiated AND background alike (the
* trash-retention purge SQL filter excludes read-only drives). Only
* `Read` passes. Admins can un-freeze via the admin-only policy PATCH.
* See `docs/plan/drive.md` §8 (`read_only`).
*/
read_only: boolean;
}
/**
@@ -0,0 +1,119 @@
<script lang="ts">
/**
* Read-only drive banner.
*
* 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
* banner and understand why upload / rename / delete / share
* affordances elsewhere in the app fail with a generic error toast —
* the backend engine gate refuses every non-`Read` permission on
* resources in the drive.
*
* Only `Read` permissions pass; the banner does not need to gate any
* behavior itself. It's pure signage. Backed by
* `docs/plan/drive.md` §8 (`read_only`).
*
* Consumed by:
* - `routes/config/drive/[uuid]/+page.svelte` — always shown when
* the drive being configured is frozen.
* - `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.
*/
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;
}
let { driveName }: 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"
>
<div class="read-only-banner__icon" aria-hidden="true">
<Icon name="lock" />
</div>
<div class="read-only-banner__body">
<strong>
{#if driveName}
{t(
'drive.read_only_banner.title_named',
{ name: driveName },
'Drive "{{name}}" is read-only'
)}
{:else}
{t('drive.read_only_banner.title', 'This drive is read-only')}
{/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.'
)}
</span>
</div>
</div>
<style>
/* Shape matches the sibling upgrade-banner in
`routes/shared-with-me/+page.svelte` so the two banners read as
the same family; only the accent shifts to communicate "info /
frozen" rather than "action / upgrade." */
.read-only-banner {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
margin-bottom: var(--space-4);
background: var(--color-surface-raised);
border: 1px solid var(--color-border);
border-left: 4px solid var(--color-accent);
border-radius: var(--radius-md);
}
.read-only-banner__icon {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border-radius: var(--radius-md);
background: var(--color-surface);
color: var(--color-accent);
font-size: var(--text-lg);
}
.read-only-banner__body {
display: flex;
flex-direction: column;
gap: var(--space-1);
min-width: 0;
}
.read-only-banner__body strong {
font-weight: var(--weight-semibold);
color: var(--color-text);
}
.read-only-banner__body span {
color: var(--color-text-muted);
font-size: var(--text-sm);
}
@media (width <= 600px) {
.read-only-banner {
align-items: flex-start;
}
}
</style>
+9
View File
@@ -111,6 +111,15 @@ export const policyDefs: PolicyDef[] = [
'admin.drive_policy.include_in_music_index_help',
'Include audio files from this drive in the Music library. Default personal drives are opted in automatically; turn on for shared drives that genuinely hold a music collection (e.g. "Family Music", "Band Collaboration").'
)
},
{
key: 'read_only',
label: () => t('admin.drive_policy.read_only', 'Read-only (freeze)'),
help: () =>
t(
'admin.drive_policy.read_only_help',
'Freeze the drive entirely — every mutation is refused (uploads, edits, deletes, renames, sharing, membership changes). Reads and downloads keep working. The trash-retention janitor also pauses. Use for archives, legal holds, or account wind-downs. Only an admin can un-freeze.'
)
}
];