@lacrous/omnicloud 0.2.26 → 0.2.28
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 +104 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +104 -2
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/server/index.js +105 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lacrous/omnicloud",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.28",
|
|
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.28",
|
|
52
|
+
"@omnicloud/shared": "0.2.28",
|
|
53
|
+
"@omnicloud/telegram": "0.2.28",
|
|
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(caption, size) {
|
|
621
|
+
if (!this.provider.findByCaption) return null;
|
|
622
|
+
try {
|
|
623
|
+
return await this.provider.findByCaption(caption, 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);
|
|
@@ -1093,6 +1102,15 @@ 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 uploadCaption(sha256, operationId) {
|
|
1109
|
+
return `${sha256}:${operationId}`;
|
|
1110
|
+
}
|
|
1111
|
+
function contentSize(input) {
|
|
1112
|
+
return input.spooled?.size ?? input.data?.byteLength ?? 0;
|
|
1113
|
+
}
|
|
1096
1114
|
function connectionCausedFailure(error) {
|
|
1097
1115
|
if (error instanceof TelegramConnectionError) return true;
|
|
1098
1116
|
if (error instanceof UploadFailedError) {
|
|
@@ -1144,6 +1162,9 @@ var FileService = class {
|
|
|
1144
1162
|
if (op?.status === "UPLOADING" && op.telegramMessageId !== null) {
|
|
1145
1163
|
return this.commitStoredObject(userId, input, operations, op);
|
|
1146
1164
|
}
|
|
1165
|
+
if (op?.status === "UNKNOWN") {
|
|
1166
|
+
return this.resolveUnknown(userId, input, operations, op);
|
|
1167
|
+
}
|
|
1147
1168
|
if (!op) {
|
|
1148
1169
|
try {
|
|
1149
1170
|
op = await operations.create({ userId, operationId, requestFingerprint: fingerprint });
|
|
@@ -1162,6 +1183,7 @@ var FileService = class {
|
|
|
1162
1183
|
}
|
|
1163
1184
|
try {
|
|
1164
1185
|
const record = await this.performUpload(userId, input, {
|
|
1186
|
+
caption: uploadCaption(contentSha256(input), op.operationId),
|
|
1165
1187
|
onStored: async (stored) => {
|
|
1166
1188
|
await operations.update(op.id, { telegramMessageId: Number(stored.messageId) });
|
|
1167
1189
|
await operations.claim(op.id, "UPLOADING", "COMMITTING");
|
|
@@ -1173,11 +1195,54 @@ var FileService = class {
|
|
|
1173
1195
|
const status = connectionCausedFailure(error) ? "UNKNOWN" : "FAILED";
|
|
1174
1196
|
await operations.update(op.id, {
|
|
1175
1197
|
status,
|
|
1198
|
+
sha256: contentSha256(input),
|
|
1199
|
+
size: contentSize(input),
|
|
1176
1200
|
error: error instanceof Error ? error.message.slice(0, 200) : "upload failed"
|
|
1177
1201
|
});
|
|
1178
1202
|
throw error;
|
|
1179
1203
|
}
|
|
1180
1204
|
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Resolves an operation whose Telegram outcome is unknown. The channel is searched
|
|
1207
|
+
* for the object by its content hash and size. Exactly one match is adopted, zero
|
|
1208
|
+
* matches means nothing was stored so the upload is sent again, and anything
|
|
1209
|
+
* ambiguous stays UNKNOWN so that no duplicate is written.
|
|
1210
|
+
*/
|
|
1211
|
+
async resolveUnknown(userId, input, operations, op) {
|
|
1212
|
+
if (op.sha256 === null || op.size === null) {
|
|
1213
|
+
throw new ConflictError(
|
|
1214
|
+
"This upload's outcome is unknown and cannot be checked automatically. Ask an administrator to reconcile it."
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1217
|
+
const engine = await this.engineFor(userId);
|
|
1218
|
+
const matches = await engine.findByCaption(uploadCaption(op.sha256, op.operationId), op.size);
|
|
1219
|
+
if (matches === null) {
|
|
1220
|
+
throw new ConflictError(
|
|
1221
|
+
"This upload's outcome is unknown and the storage backend cannot search for it."
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
if (matches.length > 1) {
|
|
1225
|
+
throw new ConflictError(
|
|
1226
|
+
"This upload's outcome is unknown and more than one matching object exists. Nothing was written."
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
if (matches.length === 1) {
|
|
1230
|
+
const stored = matches[0];
|
|
1231
|
+
await operations.update(op.id, { telegramMessageId: Number(stored.messageId) });
|
|
1232
|
+
await operations.claim(op.id, "UNKNOWN", "COMMITTING");
|
|
1233
|
+
const record = await this.commitRecord(userId, input, {
|
|
1234
|
+
name: sanitizeFileName(input.name),
|
|
1235
|
+
mimeType: mimeFromFilename(sanitizeFileName(input.name)),
|
|
1236
|
+
size: op.size,
|
|
1237
|
+
sha256: op.sha256,
|
|
1238
|
+
telegramMessageId: Number(stored.messageId)
|
|
1239
|
+
});
|
|
1240
|
+
await operations.update(op.id, { status: "COMPLETED", fileId: record.id });
|
|
1241
|
+
return record;
|
|
1242
|
+
}
|
|
1243
|
+
await operations.claim(op.id, "UNKNOWN", "FAILED");
|
|
1244
|
+
return this.uploadOnce(userId, input, op.operationId, operations);
|
|
1245
|
+
}
|
|
1181
1246
|
/**
|
|
1182
1247
|
* Follows an operation another request is driving, for a bounded time. Returns
|
|
1183
1248
|
* the finished file if the winner completes, or the stored object's commit if
|
|
@@ -1255,8 +1320,9 @@ var FileService = class {
|
|
|
1255
1320
|
mimeType,
|
|
1256
1321
|
path: input.spooled.path,
|
|
1257
1322
|
size: input.spooled.size,
|
|
1258
|
-
sha256: input.spooled.sha256
|
|
1259
|
-
|
|
1323
|
+
sha256: input.spooled.sha256,
|
|
1324
|
+
caption: hooks.caption
|
|
1325
|
+
} : { name, mimeType, data: input.data, caption: hooks.caption },
|
|
1260
1326
|
control
|
|
1261
1327
|
);
|
|
1262
1328
|
await hooks.onStored?.(stored);
|
|
@@ -2785,6 +2851,7 @@ var TelegramStorageProvider = class {
|
|
|
2785
2851
|
file,
|
|
2786
2852
|
forceDocument: true,
|
|
2787
2853
|
workers: 1,
|
|
2854
|
+
caption: input.caption,
|
|
2788
2855
|
// Surface transport-level progress to the caller. GramJS reports a
|
|
2789
2856
|
// 0..1 fraction. `isCanceled` lets us abort an in-flight upload.
|
|
2790
2857
|
progressCallback: Object.assign(
|
|
@@ -2942,6 +3009,41 @@ var TelegramStorageProvider = class {
|
|
|
2942
3009
|
}
|
|
2943
3010
|
return objects;
|
|
2944
3011
|
}
|
|
3012
|
+
/**
|
|
3013
|
+
* Finds stored documents whose caption equals the given caption and whose size
|
|
3014
|
+
* matches. Used to recover an upload whose outcome was unknown.
|
|
3015
|
+
*/
|
|
3016
|
+
async findByCaption(caption, size) {
|
|
3017
|
+
const matches = [];
|
|
3018
|
+
try {
|
|
3019
|
+
for await (const message of this.client.iterMessages(this.peer, {
|
|
3020
|
+
search: caption,
|
|
3021
|
+
limit: 20
|
|
3022
|
+
})) {
|
|
3023
|
+
if (!(message instanceof Api2.Message)) continue;
|
|
3024
|
+
if (message.message !== caption) continue;
|
|
3025
|
+
const document = message.document;
|
|
3026
|
+
if (!(document instanceof Api2.Document)) continue;
|
|
3027
|
+
if (Number(document.size.toString()) !== size) continue;
|
|
3028
|
+
let fileName;
|
|
3029
|
+
for (const attr of document.attributes) {
|
|
3030
|
+
if (attr instanceof Api2.DocumentAttributeFilename) {
|
|
3031
|
+
fileName = attr.fileName;
|
|
3032
|
+
break;
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
matches.push({
|
|
3036
|
+
messageId: message.id.toString(),
|
|
3037
|
+
name: fileName ?? "file",
|
|
3038
|
+
size,
|
|
3039
|
+
mimeType: document.mimeType
|
|
3040
|
+
});
|
|
3041
|
+
}
|
|
3042
|
+
} catch (error) {
|
|
3043
|
+
throw mapTelegramError(error, "Telegram lookup failed");
|
|
3044
|
+
}
|
|
3045
|
+
return matches;
|
|
3046
|
+
}
|
|
2945
3047
|
async stat(ref) {
|
|
2946
3048
|
const message = await this.getMessage(ref.messageId);
|
|
2947
3049
|
if (!message?.media) return null;
|
|
@@ -5095,7 +5197,7 @@ function encryptionCheck(configured) {
|
|
|
5095
5197
|
}
|
|
5096
5198
|
|
|
5097
5199
|
// ../../apps/api/src/routes/storage.ts
|
|
5098
|
-
var VERSION = "0.2.
|
|
5200
|
+
var VERSION = "0.2.28";
|
|
5099
5201
|
function registerStorageRoutes(app2, container2) {
|
|
5100
5202
|
app2.get("/api/health", async () => {
|
|
5101
5203
|
const database = await probeDatabase(container2);
|