@lacrous/omnicloud 0.2.25 → 0.2.27
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 +97 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +97 -0
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/server/index.js +99 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lacrous/omnicloud",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.27",
|
|
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.27",
|
|
52
|
+
"@omnicloud/shared": "0.2.27",
|
|
53
|
+
"@omnicloud/telegram": "0.2.27",
|
|
54
54
|
"@types/node": "^22.10.0",
|
|
55
55
|
"esbuild": "0.25.12",
|
|
56
56
|
"tsup": "^8.3.0",
|
package/server/index.js
CHANGED
|
@@ -616,6 +616,15 @@ var StorageEngine = class {
|
|
|
616
616
|
throw mapProviderError("Storage listing failed", error);
|
|
617
617
|
}
|
|
618
618
|
}
|
|
619
|
+
/** Read-only search for stored objects with this content hash and size. */
|
|
620
|
+
async findByCaption(sha256, size) {
|
|
621
|
+
if (!this.provider.findByCaption) return null;
|
|
622
|
+
try {
|
|
623
|
+
return await this.provider.findByCaption(sha256, size);
|
|
624
|
+
} catch (error) {
|
|
625
|
+
throw mapProviderError("Storage lookup failed", error);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
619
628
|
async stat(ref) {
|
|
620
629
|
try {
|
|
621
630
|
return await this.provider.stat(ref);
|
|
@@ -852,7 +861,7 @@ var ReconciliationService = class {
|
|
|
852
861
|
*/
|
|
853
862
|
async pendingUploadMessageIds(userId) {
|
|
854
863
|
const ids = /* @__PURE__ */ new Set();
|
|
855
|
-
for (const status of ["UPLOADING", "PENDING"]) {
|
|
864
|
+
for (const status of ["UPLOADING", "PENDING", "UNKNOWN"]) {
|
|
856
865
|
for (const operation of await this.repos.uploadOperations.listByStatus(userId, status)) {
|
|
857
866
|
if (operation.telegramMessageId !== null) ids.add(operation.telegramMessageId);
|
|
858
867
|
}
|
|
@@ -1093,6 +1102,12 @@ function uploadRequestFingerprint(input) {
|
|
|
1093
1102
|
}
|
|
1094
1103
|
|
|
1095
1104
|
// ../core/src/services/file-service.ts
|
|
1105
|
+
function contentSha256(input) {
|
|
1106
|
+
return input.spooled?.sha256 ?? sha256Hex(input.data ?? Buffer.alloc(0));
|
|
1107
|
+
}
|
|
1108
|
+
function contentSize(input) {
|
|
1109
|
+
return input.spooled?.size ?? input.data?.byteLength ?? 0;
|
|
1110
|
+
}
|
|
1096
1111
|
function connectionCausedFailure(error) {
|
|
1097
1112
|
if (error instanceof TelegramConnectionError) return true;
|
|
1098
1113
|
if (error instanceof UploadFailedError) {
|
|
@@ -1144,6 +1159,9 @@ var FileService = class {
|
|
|
1144
1159
|
if (op?.status === "UPLOADING" && op.telegramMessageId !== null) {
|
|
1145
1160
|
return this.commitStoredObject(userId, input, operations, op);
|
|
1146
1161
|
}
|
|
1162
|
+
if (op?.status === "UNKNOWN") {
|
|
1163
|
+
return this.resolveUnknown(userId, input, operations, op);
|
|
1164
|
+
}
|
|
1147
1165
|
if (!op) {
|
|
1148
1166
|
try {
|
|
1149
1167
|
op = await operations.create({ userId, operationId, requestFingerprint: fingerprint });
|
|
@@ -1173,11 +1191,54 @@ var FileService = class {
|
|
|
1173
1191
|
const status = connectionCausedFailure(error) ? "UNKNOWN" : "FAILED";
|
|
1174
1192
|
await operations.update(op.id, {
|
|
1175
1193
|
status,
|
|
1194
|
+
sha256: contentSha256(input),
|
|
1195
|
+
size: contentSize(input),
|
|
1176
1196
|
error: error instanceof Error ? error.message.slice(0, 200) : "upload failed"
|
|
1177
1197
|
});
|
|
1178
1198
|
throw error;
|
|
1179
1199
|
}
|
|
1180
1200
|
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Resolves an operation whose Telegram outcome is unknown. The channel is searched
|
|
1203
|
+
* for the object by its content hash and size. Exactly one match is adopted, zero
|
|
1204
|
+
* matches means nothing was stored so the upload is sent again, and anything
|
|
1205
|
+
* ambiguous stays UNKNOWN so that no duplicate is written.
|
|
1206
|
+
*/
|
|
1207
|
+
async resolveUnknown(userId, input, operations, op) {
|
|
1208
|
+
if (op.sha256 === null || op.size === null) {
|
|
1209
|
+
throw new ConflictError(
|
|
1210
|
+
"This upload's outcome is unknown and cannot be checked automatically. Ask an administrator to reconcile it."
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
const engine = await this.engineFor(userId);
|
|
1214
|
+
const matches = await engine.findByCaption(op.sha256, op.size);
|
|
1215
|
+
if (matches === null) {
|
|
1216
|
+
throw new ConflictError(
|
|
1217
|
+
"This upload's outcome is unknown and the storage backend cannot search for it."
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
if (matches.length > 1) {
|
|
1221
|
+
throw new ConflictError(
|
|
1222
|
+
"This upload's outcome is unknown and more than one matching object exists. Nothing was written."
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
if (matches.length === 1) {
|
|
1226
|
+
const stored = matches[0];
|
|
1227
|
+
await operations.update(op.id, { telegramMessageId: Number(stored.messageId) });
|
|
1228
|
+
await operations.claim(op.id, "UNKNOWN", "COMMITTING");
|
|
1229
|
+
const record = await this.commitRecord(userId, input, {
|
|
1230
|
+
name: sanitizeFileName(input.name),
|
|
1231
|
+
mimeType: mimeFromFilename(sanitizeFileName(input.name)),
|
|
1232
|
+
size: op.size,
|
|
1233
|
+
sha256: op.sha256,
|
|
1234
|
+
telegramMessageId: Number(stored.messageId)
|
|
1235
|
+
});
|
|
1236
|
+
await operations.update(op.id, { status: "COMPLETED", fileId: record.id });
|
|
1237
|
+
return record;
|
|
1238
|
+
}
|
|
1239
|
+
await operations.claim(op.id, "UNKNOWN", "FAILED");
|
|
1240
|
+
return this.uploadOnce(userId, input, op.operationId, operations);
|
|
1241
|
+
}
|
|
1181
1242
|
/**
|
|
1182
1243
|
* Follows an operation another request is driving, for a bounded time. Returns
|
|
1183
1244
|
* the finished file if the winner completes, or the stored object's commit if
|
|
@@ -2785,6 +2846,7 @@ var TelegramStorageProvider = class {
|
|
|
2785
2846
|
file,
|
|
2786
2847
|
forceDocument: true,
|
|
2787
2848
|
workers: 1,
|
|
2849
|
+
caption: input.sha256,
|
|
2788
2850
|
// Surface transport-level progress to the caller. GramJS reports a
|
|
2789
2851
|
// 0..1 fraction. `isCanceled` lets us abort an in-flight upload.
|
|
2790
2852
|
progressCallback: Object.assign(
|
|
@@ -2942,6 +3004,41 @@ var TelegramStorageProvider = class {
|
|
|
2942
3004
|
}
|
|
2943
3005
|
return objects;
|
|
2944
3006
|
}
|
|
3007
|
+
/**
|
|
3008
|
+
* Finds stored documents whose caption equals the content hash and whose size
|
|
3009
|
+
* matches. Used to recover an upload whose outcome was unknown.
|
|
3010
|
+
*/
|
|
3011
|
+
async findByCaption(sha256, size) {
|
|
3012
|
+
const matches = [];
|
|
3013
|
+
try {
|
|
3014
|
+
for await (const message of this.client.iterMessages(this.peer, {
|
|
3015
|
+
search: sha256,
|
|
3016
|
+
limit: 20
|
|
3017
|
+
})) {
|
|
3018
|
+
if (!(message instanceof Api2.Message)) continue;
|
|
3019
|
+
if (message.message !== sha256) continue;
|
|
3020
|
+
const document = message.document;
|
|
3021
|
+
if (!(document instanceof Api2.Document)) continue;
|
|
3022
|
+
if (Number(document.size.toString()) !== size) continue;
|
|
3023
|
+
let fileName;
|
|
3024
|
+
for (const attr of document.attributes) {
|
|
3025
|
+
if (attr instanceof Api2.DocumentAttributeFilename) {
|
|
3026
|
+
fileName = attr.fileName;
|
|
3027
|
+
break;
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
matches.push({
|
|
3031
|
+
messageId: message.id.toString(),
|
|
3032
|
+
name: fileName ?? "file",
|
|
3033
|
+
size,
|
|
3034
|
+
mimeType: document.mimeType
|
|
3035
|
+
});
|
|
3036
|
+
}
|
|
3037
|
+
} catch (error) {
|
|
3038
|
+
throw mapTelegramError(error, "Telegram lookup failed");
|
|
3039
|
+
}
|
|
3040
|
+
return matches;
|
|
3041
|
+
}
|
|
2945
3042
|
async stat(ref) {
|
|
2946
3043
|
const message = await this.getMessage(ref.messageId);
|
|
2947
3044
|
if (!message?.media) return null;
|
|
@@ -5095,7 +5192,7 @@ function encryptionCheck(configured) {
|
|
|
5095
5192
|
}
|
|
5096
5193
|
|
|
5097
5194
|
// ../../apps/api/src/routes/storage.ts
|
|
5098
|
-
var VERSION = "0.2.
|
|
5195
|
+
var VERSION = "0.2.27";
|
|
5099
5196
|
function registerStorageRoutes(app2, container2) {
|
|
5100
5197
|
app2.get("/api/health", async () => {
|
|
5101
5198
|
const database = await probeDatabase(container2);
|