@seedvault/server 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.html CHANGED
@@ -404,20 +404,31 @@
404
404
  const savedContributor = localStorage.getItem("sv-contributor") || "";
405
405
  const savedFile = localStorage.getItem("sv-file") || "";
406
406
 
407
- function buildTree(paths) {
407
+ function buildTree(fileEntries) {
408
408
  const root = {};
409
- for (const p of paths) {
410
- const parts = p.split("/");
409
+ for (const f of fileEntries) {
410
+ const parts = f.path.split("/");
411
411
  let node = root;
412
412
  for (const part of parts) {
413
413
  if (!node[part]) node[part] = {};
414
414
  node = node[part];
415
415
  }
416
- node.__file = p;
416
+ node.__file = f;
417
417
  }
418
418
  return root;
419
419
  }
420
420
 
421
+ function getNewestCtime(node) {
422
+ if (node.__file) return node.__file.originCtime || node.__file.serverCreatedAt || "";
423
+ let newest = "";
424
+ for (const key of Object.keys(node)) {
425
+ if (key === "__file") continue;
426
+ const t = getNewestCtime(node[key]);
427
+ if (t > newest) newest = t;
428
+ }
429
+ return newest;
430
+ }
431
+
421
432
  function countFiles(node) {
422
433
  let count = 0;
423
434
  for (const key of Object.keys(node)) {
@@ -432,6 +443,9 @@
432
443
  const aDir = Object.keys(node[a]).some((k) => k !== "__file");
433
444
  const bDir = Object.keys(node[b]).some((k) => k !== "__file");
434
445
  if (aDir !== bDir) return aDir ? -1 : 1;
446
+ const aTime = getNewestCtime(node[a]);
447
+ const bTime = getNewestCtime(node[b]);
448
+ if (aTime !== bTime) return bTime.localeCompare(aTime);
435
449
  return a.localeCompare(b);
436
450
  });
437
451
  for (const key of keys) {
@@ -444,9 +458,9 @@
444
458
 
445
459
  if (isFile) {
446
460
  row.innerHTML = '<span class="arrow">&nbsp;</span><i class="ph ph-file-text"></i> ' + key;
447
- row.dataset.path = child.__file;
461
+ row.dataset.path = child.__file.path;
448
462
  row.dataset.contributor = username;
449
- row.onclick = () => loadContent(username, child.__file, row);
463
+ row.onclick = () => loadContent(username, child.__file.path, row);
450
464
  } else {
451
465
  const sub = document.createElement("ul");
452
466
  const fileCount = countFiles(child);
@@ -522,7 +536,10 @@
522
536
  if (!silent) status("Loading files...");
523
537
  const { files } = await (await api("/v1/files?prefix=" + encodeURIComponent(username + "/"))).json();
524
538
  const prefix = username + "/";
525
- const tree = buildTree(files.map((f) => f.path.startsWith(prefix) ? f.path.slice(prefix.length) : f.path));
539
+ const tree = buildTree(files.map((f) => ({
540
+ ...f,
541
+ path: f.path.startsWith(prefix) ? f.path.slice(prefix.length) : f.path,
542
+ })));
526
543
  renderTree(tree, sub, username, 1);
527
544
  markActiveRow();
528
545
  if (!silent) status(files.length + " file(s)");
package/dist/server.js CHANGED
@@ -40,6 +40,17 @@ function initDb(dbPath) {
40
40
  used_at TEXT,
41
41
  used_by TEXT REFERENCES contributors(username)
42
42
  );
43
+
44
+ CREATE TABLE IF NOT EXISTS files (
45
+ contributor TEXT NOT NULL,
46
+ path TEXT NOT NULL,
47
+ origin_ctime TEXT NOT NULL,
48
+ origin_mtime TEXT NOT NULL,
49
+ server_created_at TEXT NOT NULL,
50
+ server_modified_at TEXT NOT NULL,
51
+ PRIMARY KEY (contributor, path),
52
+ FOREIGN KEY (contributor) REFERENCES contributors(username)
53
+ );
43
54
  `);
44
55
  return db;
45
56
  }
@@ -98,6 +109,34 @@ function getInvite(id) {
98
109
  function markInviteUsed(id, usedBy) {
99
110
  getDb().prepare("UPDATE invites SET used_at = ?, used_by = ? WHERE id = ?").run(new Date().toISOString(), usedBy, id);
100
111
  }
112
+ function upsertFileMetadata(contributor, path, originCtime, originMtime) {
113
+ const now = new Date().toISOString();
114
+ getDb().prepare(`INSERT INTO files (contributor, path, origin_ctime, origin_mtime, server_created_at, server_modified_at)
115
+ VALUES (?, ?, ?, ?, ?, ?)
116
+ ON CONFLICT (contributor, path) DO UPDATE SET
117
+ origin_mtime = excluded.origin_mtime,
118
+ server_modified_at = excluded.server_modified_at`).run(contributor, path, originCtime, originMtime, now, now);
119
+ return getFileMetadata(contributor, path);
120
+ }
121
+ function getFileMetadata(contributor, path) {
122
+ return getDb().prepare("SELECT * FROM files WHERE contributor = ? AND path = ?").get(contributor, path);
123
+ }
124
+ function listFileMetadata(contributor, prefix) {
125
+ const map = new Map;
126
+ let rows;
127
+ if (prefix) {
128
+ rows = getDb().prepare("SELECT * FROM files WHERE contributor = ? AND path LIKE ?").all(contributor, prefix + "%");
129
+ } else {
130
+ rows = getDb().prepare("SELECT * FROM files WHERE contributor = ?").all(contributor);
131
+ }
132
+ for (const row of rows) {
133
+ map.set(row.path, row);
134
+ }
135
+ return map;
136
+ }
137
+ function deleteFileMetadata(contributor, path) {
138
+ getDb().prepare("DELETE FROM files WHERE contributor = ? AND path = ?").run(contributor, path);
139
+ }
101
140
 
102
141
  // ../node_modules/.bun/hono@4.11.9/node_modules/hono/dist/compose.js
103
142
  var compose = (middleware, onError, onNotFound) => {
@@ -2128,8 +2167,12 @@ function createApp(storageRoot) {
2128
2167
  return c.json({ error: pathError }, 400);
2129
2168
  }
2130
2169
  const content = await c.req.text();
2170
+ const now = new Date().toISOString();
2171
+ const originCtime = c.req.header("X-Origin-Ctime") || now;
2172
+ const originMtime = c.req.header("X-Origin-Mtime") || now;
2131
2173
  try {
2132
2174
  const result = await writeFileAtomic(storageRoot, parsed.username, parsed.filePath, content);
2175
+ const meta = upsertFileMetadata(parsed.username, parsed.filePath, originCtime, originMtime);
2133
2176
  broadcast("file_updated", {
2134
2177
  contributor: parsed.username,
2135
2178
  path: result.path,
@@ -2137,7 +2180,13 @@ function createApp(storageRoot) {
2137
2180
  modifiedAt: result.modifiedAt
2138
2181
  });
2139
2182
  triggerUpdate();
2140
- return c.json(result);
2183
+ return c.json({
2184
+ ...result,
2185
+ originCtime: meta.origin_ctime,
2186
+ originMtime: meta.origin_mtime,
2187
+ serverCreatedAt: meta.server_created_at,
2188
+ serverModifiedAt: meta.server_modified_at
2189
+ });
2141
2190
  } catch (e) {
2142
2191
  if (e instanceof FileTooLargeError) {
2143
2192
  return c.json({ error: e.message }, 413);
@@ -2160,6 +2209,7 @@ function createApp(storageRoot) {
2160
2209
  }
2161
2210
  try {
2162
2211
  await deleteFile(storageRoot, parsed.username, parsed.filePath);
2212
+ deleteFileMetadata(parsed.username, parsed.filePath);
2163
2213
  broadcast("file_deleted", {
2164
2214
  contributor: parsed.username,
2165
2215
  path: parsed.filePath
@@ -2185,11 +2235,19 @@ function createApp(storageRoot) {
2185
2235
  return c.json({ error: "Contributor not found" }, 404);
2186
2236
  }
2187
2237
  const files = await listFiles(storageRoot, username, subPrefix);
2238
+ const metaMap = listFileMetadata(username, subPrefix);
2188
2239
  return c.json({
2189
- files: files.map((f) => ({
2190
- ...f,
2191
- path: `${username}/${f.path}`
2192
- }))
2240
+ files: files.map((f) => {
2241
+ const meta = metaMap.get(f.path);
2242
+ return {
2243
+ ...f,
2244
+ path: `${username}/${f.path}`,
2245
+ originCtime: meta?.origin_ctime,
2246
+ originMtime: meta?.origin_mtime,
2247
+ serverCreatedAt: meta?.server_created_at,
2248
+ serverModifiedAt: meta?.server_modified_at
2249
+ };
2250
+ })
2193
2251
  });
2194
2252
  });
2195
2253
  authed.get("/v1/events", (c) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seedvault/server",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "seedvault-server": "bin/seedvault-server.mjs"