@lacrous/omnicloud 0.2.9 → 0.2.11
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.cjs +44 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/server/index.js +79 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lacrous/omnicloud",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
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.
|
|
52
|
-
"@omnicloud/shared": "0.2.
|
|
53
|
-
"@omnicloud/telegram": "0.2.
|
|
51
|
+
"@omnicloud/core": "0.2.11",
|
|
52
|
+
"@omnicloud/shared": "0.2.11",
|
|
53
|
+
"@omnicloud/telegram": "0.2.11",
|
|
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);
|
|
@@ -918,7 +935,9 @@ var ReconciliationApplier = class {
|
|
|
918
935
|
const file = (await this.repos.files.listByUser(this.userId)).find(
|
|
919
936
|
(candidate) => candidate.telegramMessageId === action.messageId
|
|
920
937
|
);
|
|
921
|
-
if (!file)
|
|
938
|
+
if (!file) {
|
|
939
|
+
throw new Error("No file of yours references this message, so there is nothing to detach");
|
|
940
|
+
}
|
|
922
941
|
await this.repos.files.update(file.id, { deletedAt: /* @__PURE__ */ new Date(), trashBatchId: null });
|
|
923
942
|
}
|
|
924
943
|
async adopt(action) {
|
|
@@ -1538,6 +1557,33 @@ var FileService = class {
|
|
|
1538
1557
|
};
|
|
1539
1558
|
}
|
|
1540
1559
|
// ── internals ────────────────────────────────────────────────────────────
|
|
1560
|
+
/**
|
|
1561
|
+
* Removes historical versions the policy no longer keeps. For each version the
|
|
1562
|
+
* Telegram message is deleted first, and its row only after that succeeds, the
|
|
1563
|
+
* same order as a permanent delete. A failed remote delete leaves that version
|
|
1564
|
+
* in place. The current version is never removed. Returns how many were removed.
|
|
1565
|
+
*/
|
|
1566
|
+
async pruneVersions(userId, fileId, policy, now = /* @__PURE__ */ new Date()) {
|
|
1567
|
+
const record = await this.get(userId, fileId);
|
|
1568
|
+
const versions = await this.files.listVersions(fileId);
|
|
1569
|
+
const doomed = versionsToPrune(versions, record.currentVersionId ?? null, policy, now);
|
|
1570
|
+
if (doomed.length === 0) return 0;
|
|
1571
|
+
const engine = await this.engineFor(userId);
|
|
1572
|
+
let removed = 0;
|
|
1573
|
+
for (const version of doomed) {
|
|
1574
|
+
await engine.remove({ messageId: String(version.telegramMessageId) });
|
|
1575
|
+
await this.files.deleteVersion(version.id);
|
|
1576
|
+
removed += 1;
|
|
1577
|
+
}
|
|
1578
|
+
await this.activity.record({
|
|
1579
|
+
userId,
|
|
1580
|
+
action: "delete",
|
|
1581
|
+
resourceType: "file",
|
|
1582
|
+
resourceId: fileId,
|
|
1583
|
+
resourceName: `${record.name} (${removed} old version${removed === 1 ? "" : "s"})`
|
|
1584
|
+
});
|
|
1585
|
+
return removed;
|
|
1586
|
+
}
|
|
1541
1587
|
async assertFolder(userId, folderId) {
|
|
1542
1588
|
const folder = await this.folders.findById(folderId);
|
|
1543
1589
|
if (!folder || folder.userId !== userId) {
|
|
@@ -3870,6 +3916,9 @@ function createFileRepo(prisma) {
|
|
|
3870
3916
|
async deleteVersionsByFileIds(fileIds) {
|
|
3871
3917
|
if (fileIds.length === 0) return;
|
|
3872
3918
|
await prisma.fileVersion.deleteMany({ where: { fileId: { in: fileIds } } });
|
|
3919
|
+
},
|
|
3920
|
+
async deleteVersion(versionId) {
|
|
3921
|
+
await prisma.fileVersion.deleteMany({ where: { id: versionId } });
|
|
3873
3922
|
}
|
|
3874
3923
|
};
|
|
3875
3924
|
}
|
|
@@ -4317,6 +4366,27 @@ function parseListQuery(raw) {
|
|
|
4317
4366
|
}
|
|
4318
4367
|
return { query, page: query.page, limit: query.limit };
|
|
4319
4368
|
}
|
|
4369
|
+
function parseRetentionPolicy(body) {
|
|
4370
|
+
const kind = requireString(body, "policy");
|
|
4371
|
+
if (kind === "KEEP_ALL") return { kind: "KEEP_ALL" };
|
|
4372
|
+
if (kind === "KEEP_LATEST_N") {
|
|
4373
|
+
const count = requireWholeNumber(body, "count");
|
|
4374
|
+
return { kind: "KEEP_LATEST_N", count };
|
|
4375
|
+
}
|
|
4376
|
+
if (kind === "KEEP_FOR_DAYS") {
|
|
4377
|
+
const days = requireWholeNumber(body, "days");
|
|
4378
|
+
if (days < 1) throw new ValidationError("days must be at least 1");
|
|
4379
|
+
return { kind: "KEEP_FOR_DAYS", days };
|
|
4380
|
+
}
|
|
4381
|
+
throw new ValidationError("policy must be KEEP_ALL, KEEP_LATEST_N or KEEP_FOR_DAYS");
|
|
4382
|
+
}
|
|
4383
|
+
function requireWholeNumber(body, field) {
|
|
4384
|
+
const value = body?.[field];
|
|
4385
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
4386
|
+
throw new ValidationError(`${field} must be a whole number of zero or more`);
|
|
4387
|
+
}
|
|
4388
|
+
return value;
|
|
4389
|
+
}
|
|
4320
4390
|
|
|
4321
4391
|
// ../../apps/api/src/mappers.ts
|
|
4322
4392
|
function toUserDTO(user) {
|
|
@@ -4637,6 +4707,13 @@ function registerFileRoutes(app2, container2) {
|
|
|
4637
4707
|
const versions = await container2.files.listVersions(request.user.id, id);
|
|
4638
4708
|
return { versions: versions.map((v) => toVersionDTO(v, record.currentVersionId)) };
|
|
4639
4709
|
});
|
|
4710
|
+
app2.post("/api/files/:id/versions/prune", async (request, reply) => {
|
|
4711
|
+
const { id } = request.params;
|
|
4712
|
+
const body = requireBody(request);
|
|
4713
|
+
const policy = parseRetentionPolicy(body);
|
|
4714
|
+
const removed = await container2.files.pruneVersions(request.user.id, id, policy);
|
|
4715
|
+
return reply.send({ removed });
|
|
4716
|
+
});
|
|
4640
4717
|
app2.post("/api/files/:id/replace", async (request, reply) => {
|
|
4641
4718
|
const { id } = request.params;
|
|
4642
4719
|
const part = await request.file();
|
|
@@ -4895,7 +4972,7 @@ function encryptionCheck(configured) {
|
|
|
4895
4972
|
}
|
|
4896
4973
|
|
|
4897
4974
|
// ../../apps/api/src/routes/storage.ts
|
|
4898
|
-
var VERSION = "0.2.
|
|
4975
|
+
var VERSION = "0.2.11";
|
|
4899
4976
|
function registerStorageRoutes(app2, container2) {
|
|
4900
4977
|
app2.get("/api/health", async () => {
|
|
4901
4978
|
const database = await probeDatabase(container2);
|