Skip to content

Commit dfaff53

Browse files
feat(webdav): one OpenCoperLock.txt at the Drive ROOT (not per-space)
Corrects the previous per-space note. There is now a single, deletable OpenCoperLock.txt at the personal Drive root - the place WebDAV/API let you drop loose files that the web app intentionally does not show. It explains that root files stay invisible in the app and to keep files inside spaces. - Seeded once, lazily, on the first WebDAV root PROPFIND (new User flag rootReadmeSeeded, so deleting the note keeps it gone; a transient quota/storage error just retries next time). - The ability to place files at the root is untouched. - Reverted the space-creation seeding from the previous commit.
1 parent f98cf7e commit dfaff53

5 files changed

Lines changed: 60 additions & 45 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- One-time flag: has the OpenCoperLock.txt readme been placed at the user's Drive root yet.
2+
ALTER TABLE "User" ADD COLUMN "rootReadmeSeeded" BOOLEAN NOT NULL DEFAULT false;

apps/api/prisma/schema.prisma

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ model User {
7575
// The build (git SHA) for which this user last dismissed the "What's new" dialog. null until
7676
// they first load the app; lets us show release notes exactly once per update, per user.
7777
lastSeenVersion String?
78+
// Whether the one-time OpenCoperLock.txt readme has been placed at the Drive root. Seeded once
79+
// (on first WebDAV root listing or registration); a flag, not an existence check, so deleting the
80+
// file makes it stay gone.
81+
rootReadmeSeeded Boolean @default(false)
7882
createdAt DateTime @default(now())
7983
updatedAt DateTime @updatedAt
8084

apps/api/src/routes/folders.ts

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,10 @@
11
import type { FastifyPluginAsync } from 'fastify';
2-
import { Readable } from 'node:stream';
3-
import type { Folder } from '@prisma/client';
42
import { createFolderSchema, randomToken, updateFolderSchema } from '@opencoperlock/shared';
53
import { prisma } from '../db.js';
64
import { parseOr400 } from '../lib/validate.js';
75
import { toPublicFolder } from '../lib/serialize.js';
86
import { hardDeleteFolder, trashFolder } from '../services/trash.js';
9-
import { storeUserFile } from '../services/upload.js';
107
import { audit } from '../services/audit.js';
11-
import type { AppContext } from '../context.js';
12-
13-
/** Bilingual welcome note dropped into the root of each new space. Users may delete it freely. */
14-
function welcomeText(space: Folder): string {
15-
const mode = space.isPublic
16-
? 'Public / Open (plaintext, direct URLs)'
17-
: 'Server-encrypted (AES-256-GCM at rest)';
18-
return [
19-
`OpenCoperLock - "${space.name}"`,
20-
'='.repeat(40),
21-
'',
22-
`[FR] Ceci est un espace (${mode}). Les fichiers que vous deposez ICI apparaissent`,
23-
'dans l\'application web OpenCoperLock. Bon a savoir : un fichier place a la RACINE du',
24-
'Drive via WebDAV ou l\'API ne s\'affiche PAS dans l\'app web - rangez toujours vos',
25-
'fichiers dans un espace comme celui-ci. Vous pouvez supprimer ce fichier sans risque.',
26-
'',
27-
`[EN] This is a space (${mode}). Files you drop HERE show up in the OpenCoperLock web`,
28-
'app. Heads up: a file placed at the Drive ROOT over WebDAV or the API does NOT appear',
29-
'in the web app - always keep your files inside a space like this one. You can delete',
30-
'this file safely.',
31-
'',
32-
].join('\n');
33-
}
34-
35-
/** Best-effort: put an OpenCoperLock.txt in a brand-new space's root. Never fails folder creation. */
36-
async function seedWelcomeFile(ctx: AppContext, ownerId: string, space: Folder): Promise<void> {
37-
try {
38-
await storeUserFile(ctx, {
39-
ownerId,
40-
folderId: space.id,
41-
stream: Readable.from([Buffer.from(welcomeText(space), 'utf8')]),
42-
filename: 'OpenCoperLock.txt',
43-
mimetype: 'text/plain',
44-
public: space.isPublic,
45-
});
46-
} catch {
47-
/* quota exhausted, storage hiccup, etc. - the space is still created. */
48-
}
49-
}
508

519
/** Collect a folder's id plus all descendant ids (breadth-first), scoped to one owner's personal
5210
* Drive (Shared-Space folders carry a spaceId and are handled by the /spaces routes). */
@@ -112,9 +70,6 @@ export const folderRoutes: FastifyPluginAsync = async (app) => {
11270
},
11371
});
11472
await audit(req, 'folder.create', { target: folder.id });
115-
// A brand-new top-level space (not a subfolder, not a blind ZK vault) gets a short readme in
116-
// its root explaining how spaces work. Best-effort and deletable.
117-
if (!body.parentId && !isZk) await seedWelcomeFile(app.ctx, req.user!.id, folder);
11873
return reply.code(201).send({ folder: toPublicFolder(folder) });
11974
} catch {
12075
return reply.code(409).send({ error: 'A folder with that name already exists here' });

apps/api/src/routes/webdav.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { storeUserFile, QuotaExhaustedError } from '../services/upload.js';
2020
import { FileTooLargeError, InfectedFileError } from '../services/ingest.js';
2121
import { trashFile, trashFolder } from '../services/trash.js';
2222
import { remainingAllowance } from '../services/quota.js';
23+
import { ensureRootReadme } from '../services/welcome.js';
2324

2425
const PREFIX = '/dav';
2526
const READ_METHODS = new Set(['GET', 'HEAD', 'OPTIONS', 'PROPFIND']);
@@ -186,6 +187,9 @@ export const webdavRoutes: FastifyPluginAsync = async (app) => {
186187

187188
if (method === 'PROPFIND') {
188189
if (node.type === 'invalid' || node.type === 'missing') return reply.code(404).send();
190+
// First time the root is browsed, drop a one-time OpenCoperLock.txt there (before we list, so
191+
// it shows in this very response). Deleting it keeps it gone.
192+
if (node.type === 'root') await ensureRootReadme(app.ctx, ownerId);
189193
const depth = String(req.headers.depth ?? '1');
190194
const parts: string[] = [];
191195
// Report the account quota on the queried collection so the mount shows the right size.

apps/api/src/services/welcome.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Readable } from 'node:stream';
2+
import { prisma } from '../db.js';
3+
import { storeUserFile } from './upload.js';
4+
import type { AppContext } from '../context.js';
5+
6+
/** The one-time note placed at the Drive ROOT (folderId = null). It's reachable over WebDAV / the
7+
* API, where you *can* drop loose files at the root - unlike the web app, which only shows spaces
8+
* (top-level folders). Kept ASCII so it reads cleanly in any editor. */
9+
const ROOT_README = [
10+
'OpenCoperLock - Drive root',
11+
'='.repeat(40),
12+
'',
13+
'[FR] Vous etes a la RACINE de votre Drive (accessible via WebDAV et l\'API).',
14+
'Les dossiers que vous voyez ici sont vos ESPACES. Vous pouvez deposer des fichiers',
15+
'directement a cette racine (comme ce .txt), MAIS ces fichiers-la ne s\'affichent PAS',
16+
'dans l\'application web OpenCoperLock - seuls les espaces (dossiers) y apparaissent.',
17+
'Pour retrouver vos fichiers dans l\'app, rangez-les dans un espace. Vous pouvez',
18+
'supprimer ce fichier sans risque (il ne sera pas recree).',
19+
'',
20+
'[EN] You are at the ROOT of your Drive (reachable over WebDAV and the API).',
21+
'The folders you see here are your SPACES. You can drop files directly at this root',
22+
'(like this .txt), BUT those root files do NOT show in the OpenCoperLock web app -',
23+
'only spaces (folders) appear there. To find your files in the app, keep them inside',
24+
'a space. You can delete this file safely (it will not be recreated).',
25+
'',
26+
].join('\n');
27+
28+
/** Place OpenCoperLock.txt at the user's Drive root exactly once. Idempotent and best-effort: it
29+
* never throws, and the "seeded" flag (not a file-existence check) means a user who deletes the
30+
* note keeps it gone. */
31+
export async function ensureRootReadme(ctx: AppContext, ownerId: string): Promise<void> {
32+
try {
33+
const user = await prisma.user.findUnique({
34+
where: { id: ownerId },
35+
select: { rootReadmeSeeded: true },
36+
});
37+
if (!user || user.rootReadmeSeeded) return;
38+
await storeUserFile(ctx, {
39+
ownerId,
40+
folderId: null,
41+
stream: Readable.from([Buffer.from(ROOT_README, 'utf8')]),
42+
filename: 'OpenCoperLock.txt',
43+
mimetype: 'text/plain',
44+
});
45+
// Only mark as seeded once the write succeeds, so a transient quota/storage error retries later.
46+
await prisma.user.update({ where: { id: ownerId }, data: { rootReadmeSeeded: true } });
47+
} catch {
48+
/* quota exhausted, storage hiccup, race - harmless; we'll try again next time. */
49+
}
50+
}

0 commit comments

Comments
 (0)