@lacrous/omnicloud 0.2.8 → 0.2.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lacrous/omnicloud",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "SDK and core building blocks for OmniCloud — self-hosted cloud storage powered by your Telegram account.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -48,9 +48,9 @@
48
48
  "prisma": "^6.2.0"
49
49
  },
50
50
  "devDependencies": {
51
- "@omnicloud/core": "0.2.8",
52
- "@omnicloud/shared": "0.2.8",
53
- "@omnicloud/telegram": "0.2.8",
51
+ "@omnicloud/core": "0.2.10",
52
+ "@omnicloud/shared": "0.2.10",
53
+ "@omnicloud/telegram": "0.2.10",
54
54
  "@types/node": "^22.10.0",
55
55
  "esbuild": "0.25.12",
56
56
  "tsup": "^8.3.0",
package/server/index.js CHANGED
@@ -761,6 +761,23 @@ function truncate(value, max) {
761
761
  return value === null ? null : value.slice(0, max);
762
762
  }
763
763
 
764
+ // ../core/src/services/version-retention.ts
765
+ function versionsToPrune(versions, currentVersionId, policy, now) {
766
+ const historical = versions.filter((version) => version.id !== currentVersionId).sort((a, b) => b.versionNumber - a.versionNumber);
767
+ switch (policy.kind) {
768
+ case "KEEP_ALL":
769
+ return [];
770
+ case "KEEP_LATEST_N": {
771
+ const keep = Math.max(0, policy.count);
772
+ return historical.slice(keep);
773
+ }
774
+ case "KEEP_FOR_DAYS": {
775
+ const cutoff = now.getTime() - policy.days * 24 * 60 * 60 * 1e3;
776
+ return historical.filter((version) => version.createdAt.getTime() < cutoff);
777
+ }
778
+ }
779
+ }
780
+
764
781
  // ../core/src/services/upload-operation.ts
765
782
  function isValidOperationId(id) {
766
783
  return /^[A-Za-z0-9_-]{8,64}$/.test(id);
@@ -1538,6 +1555,33 @@ var FileService = class {
1538
1555
  };
1539
1556
  }
1540
1557
  // ── internals ────────────────────────────────────────────────────────────
1558
+ /**
1559
+ * Removes historical versions the policy no longer keeps. For each version the
1560
+ * Telegram message is deleted first, and its row only after that succeeds, the
1561
+ * same order as a permanent delete. A failed remote delete leaves that version
1562
+ * in place. The current version is never removed. Returns how many were removed.
1563
+ */
1564
+ async pruneVersions(userId, fileId, policy, now = /* @__PURE__ */ new Date()) {
1565
+ const record = await this.get(userId, fileId);
1566
+ const versions = await this.files.listVersions(fileId);
1567
+ const doomed = versionsToPrune(versions, record.currentVersionId ?? null, policy, now);
1568
+ if (doomed.length === 0) return 0;
1569
+ const engine = await this.engineFor(userId);
1570
+ let removed = 0;
1571
+ for (const version of doomed) {
1572
+ await engine.remove({ messageId: String(version.telegramMessageId) });
1573
+ await this.files.deleteVersion(version.id);
1574
+ removed += 1;
1575
+ }
1576
+ await this.activity.record({
1577
+ userId,
1578
+ action: "delete",
1579
+ resourceType: "file",
1580
+ resourceId: fileId,
1581
+ resourceName: `${record.name} (${removed} old version${removed === 1 ? "" : "s"})`
1582
+ });
1583
+ return removed;
1584
+ }
1541
1585
  async assertFolder(userId, folderId) {
1542
1586
  const folder = await this.folders.findById(folderId);
1543
1587
  if (!folder || folder.userId !== userId) {
@@ -3870,6 +3914,9 @@ function createFileRepo(prisma) {
3870
3914
  async deleteVersionsByFileIds(fileIds) {
3871
3915
  if (fileIds.length === 0) return;
3872
3916
  await prisma.fileVersion.deleteMany({ where: { fileId: { in: fileIds } } });
3917
+ },
3918
+ async deleteVersion(versionId) {
3919
+ await prisma.fileVersion.deleteMany({ where: { id: versionId } });
3873
3920
  }
3874
3921
  };
3875
3922
  }
@@ -4317,6 +4364,27 @@ function parseListQuery(raw) {
4317
4364
  }
4318
4365
  return { query, page: query.page, limit: query.limit };
4319
4366
  }
4367
+ function parseRetentionPolicy(body) {
4368
+ const kind = requireString(body, "policy");
4369
+ if (kind === "KEEP_ALL") return { kind: "KEEP_ALL" };
4370
+ if (kind === "KEEP_LATEST_N") {
4371
+ const count = requireWholeNumber(body, "count");
4372
+ return { kind: "KEEP_LATEST_N", count };
4373
+ }
4374
+ if (kind === "KEEP_FOR_DAYS") {
4375
+ const days = requireWholeNumber(body, "days");
4376
+ if (days < 1) throw new ValidationError("days must be at least 1");
4377
+ return { kind: "KEEP_FOR_DAYS", days };
4378
+ }
4379
+ throw new ValidationError("policy must be KEEP_ALL, KEEP_LATEST_N or KEEP_FOR_DAYS");
4380
+ }
4381
+ function requireWholeNumber(body, field) {
4382
+ const value = body?.[field];
4383
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
4384
+ throw new ValidationError(`${field} must be a whole number of zero or more`);
4385
+ }
4386
+ return value;
4387
+ }
4320
4388
 
4321
4389
  // ../../apps/api/src/mappers.ts
4322
4390
  function toUserDTO(user) {
@@ -4637,6 +4705,13 @@ function registerFileRoutes(app2, container2) {
4637
4705
  const versions = await container2.files.listVersions(request.user.id, id);
4638
4706
  return { versions: versions.map((v) => toVersionDTO(v, record.currentVersionId)) };
4639
4707
  });
4708
+ app2.post("/api/files/:id/versions/prune", async (request, reply) => {
4709
+ const { id } = request.params;
4710
+ const body = requireBody(request);
4711
+ const policy = parseRetentionPolicy(body);
4712
+ const removed = await container2.files.pruneVersions(request.user.id, id, policy);
4713
+ return reply.send({ removed });
4714
+ });
4640
4715
  app2.post("/api/files/:id/replace", async (request, reply) => {
4641
4716
  const { id } = request.params;
4642
4717
  const part = await request.file();
@@ -4895,7 +4970,7 @@ function encryptionCheck(configured) {
4895
4970
  }
4896
4971
 
4897
4972
  // ../../apps/api/src/routes/storage.ts
4898
- var VERSION = "0.2.8";
4973
+ var VERSION = "0.2.10";
4899
4974
  function registerStorageRoutes(app2, container2) {
4900
4975
  app2.get("/api/health", async () => {
4901
4976
  const database = await probeDatabase(container2);