@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/dist/index.cjs
CHANGED
|
@@ -1282,6 +1282,15 @@ var StorageEngine = class {
|
|
|
1282
1282
|
throw mapProviderError("Storage listing failed", error);
|
|
1283
1283
|
}
|
|
1284
1284
|
}
|
|
1285
|
+
/** Read-only search for stored objects with this content hash and size. */
|
|
1286
|
+
async findByCaption(sha256, size) {
|
|
1287
|
+
if (!this.provider.findByCaption) return null;
|
|
1288
|
+
try {
|
|
1289
|
+
return await this.provider.findByCaption(sha256, size);
|
|
1290
|
+
} catch (error) {
|
|
1291
|
+
throw mapProviderError("Storage lookup failed", error);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1285
1294
|
async stat(ref) {
|
|
1286
1295
|
try {
|
|
1287
1296
|
return await this.provider.stat(ref);
|
|
@@ -1500,6 +1509,12 @@ function uploadRequestFingerprint(input) {
|
|
|
1500
1509
|
}
|
|
1501
1510
|
|
|
1502
1511
|
// ../core/src/services/file-service.ts
|
|
1512
|
+
function contentSha256(input) {
|
|
1513
|
+
return input.spooled?.sha256 ?? sha256Hex(input.data ?? Buffer.alloc(0));
|
|
1514
|
+
}
|
|
1515
|
+
function contentSize(input) {
|
|
1516
|
+
return input.spooled?.size ?? input.data?.byteLength ?? 0;
|
|
1517
|
+
}
|
|
1503
1518
|
function connectionCausedFailure(error) {
|
|
1504
1519
|
if (error instanceof TelegramConnectionError) return true;
|
|
1505
1520
|
if (error instanceof UploadFailedError) {
|
|
@@ -1557,6 +1572,9 @@ var FileService = class {
|
|
|
1557
1572
|
if (op?.status === "UPLOADING" && op.telegramMessageId !== null) {
|
|
1558
1573
|
return this.commitStoredObject(userId, input, operations, op);
|
|
1559
1574
|
}
|
|
1575
|
+
if (op?.status === "UNKNOWN") {
|
|
1576
|
+
return this.resolveUnknown(userId, input, operations, op);
|
|
1577
|
+
}
|
|
1560
1578
|
if (!op) {
|
|
1561
1579
|
try {
|
|
1562
1580
|
op = await operations.create({ userId, operationId, requestFingerprint: fingerprint });
|
|
@@ -1586,11 +1604,54 @@ var FileService = class {
|
|
|
1586
1604
|
const status = connectionCausedFailure(error) ? "UNKNOWN" : "FAILED";
|
|
1587
1605
|
await operations.update(op.id, {
|
|
1588
1606
|
status,
|
|
1607
|
+
sha256: contentSha256(input),
|
|
1608
|
+
size: contentSize(input),
|
|
1589
1609
|
error: error instanceof Error ? error.message.slice(0, 200) : "upload failed"
|
|
1590
1610
|
});
|
|
1591
1611
|
throw error;
|
|
1592
1612
|
}
|
|
1593
1613
|
}
|
|
1614
|
+
/**
|
|
1615
|
+
* Resolves an operation whose Telegram outcome is unknown. The channel is searched
|
|
1616
|
+
* for the object by its content hash and size. Exactly one match is adopted, zero
|
|
1617
|
+
* matches means nothing was stored so the upload is sent again, and anything
|
|
1618
|
+
* ambiguous stays UNKNOWN so that no duplicate is written.
|
|
1619
|
+
*/
|
|
1620
|
+
async resolveUnknown(userId, input, operations, op) {
|
|
1621
|
+
if (op.sha256 === null || op.size === null) {
|
|
1622
|
+
throw new ConflictError(
|
|
1623
|
+
"This upload's outcome is unknown and cannot be checked automatically. Ask an administrator to reconcile it."
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1626
|
+
const engine = await this.engineFor(userId);
|
|
1627
|
+
const matches = await engine.findByCaption(op.sha256, op.size);
|
|
1628
|
+
if (matches === null) {
|
|
1629
|
+
throw new ConflictError(
|
|
1630
|
+
"This upload's outcome is unknown and the storage backend cannot search for it."
|
|
1631
|
+
);
|
|
1632
|
+
}
|
|
1633
|
+
if (matches.length > 1) {
|
|
1634
|
+
throw new ConflictError(
|
|
1635
|
+
"This upload's outcome is unknown and more than one matching object exists. Nothing was written."
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1638
|
+
if (matches.length === 1) {
|
|
1639
|
+
const stored = matches[0];
|
|
1640
|
+
await operations.update(op.id, { telegramMessageId: Number(stored.messageId) });
|
|
1641
|
+
await operations.claim(op.id, "UNKNOWN", "COMMITTING");
|
|
1642
|
+
const record = await this.commitRecord(userId, input, {
|
|
1643
|
+
name: sanitizeFileName(input.name),
|
|
1644
|
+
mimeType: mimeFromFilename(sanitizeFileName(input.name)),
|
|
1645
|
+
size: op.size,
|
|
1646
|
+
sha256: op.sha256,
|
|
1647
|
+
telegramMessageId: Number(stored.messageId)
|
|
1648
|
+
});
|
|
1649
|
+
await operations.update(op.id, { status: "COMPLETED", fileId: record.id });
|
|
1650
|
+
return record;
|
|
1651
|
+
}
|
|
1652
|
+
await operations.claim(op.id, "UNKNOWN", "FAILED");
|
|
1653
|
+
return this.uploadOnce(userId, input, op.operationId, operations);
|
|
1654
|
+
}
|
|
1594
1655
|
/**
|
|
1595
1656
|
* Follows an operation another request is driving, for a bounded time. Returns
|
|
1596
1657
|
* the finished file if the winner completes, or the stored object's commit if
|
|
@@ -3037,6 +3098,7 @@ var TelegramStorageProvider = class {
|
|
|
3037
3098
|
file,
|
|
3038
3099
|
forceDocument: true,
|
|
3039
3100
|
workers: 1,
|
|
3101
|
+
caption: input.sha256,
|
|
3040
3102
|
// Surface transport-level progress to the caller. GramJS reports a
|
|
3041
3103
|
// 0..1 fraction. `isCanceled` lets us abort an in-flight upload.
|
|
3042
3104
|
progressCallback: Object.assign(
|
|
@@ -3194,6 +3256,41 @@ var TelegramStorageProvider = class {
|
|
|
3194
3256
|
}
|
|
3195
3257
|
return objects;
|
|
3196
3258
|
}
|
|
3259
|
+
/**
|
|
3260
|
+
* Finds stored documents whose caption equals the content hash and whose size
|
|
3261
|
+
* matches. Used to recover an upload whose outcome was unknown.
|
|
3262
|
+
*/
|
|
3263
|
+
async findByCaption(sha256, size) {
|
|
3264
|
+
const matches = [];
|
|
3265
|
+
try {
|
|
3266
|
+
for await (const message of this.client.iterMessages(this.peer, {
|
|
3267
|
+
search: sha256,
|
|
3268
|
+
limit: 20
|
|
3269
|
+
})) {
|
|
3270
|
+
if (!(message instanceof import_telegram2.Api.Message)) continue;
|
|
3271
|
+
if (message.message !== sha256) continue;
|
|
3272
|
+
const document = message.document;
|
|
3273
|
+
if (!(document instanceof import_telegram2.Api.Document)) continue;
|
|
3274
|
+
if (Number(document.size.toString()) !== size) continue;
|
|
3275
|
+
let fileName;
|
|
3276
|
+
for (const attr of document.attributes) {
|
|
3277
|
+
if (attr instanceof import_telegram2.Api.DocumentAttributeFilename) {
|
|
3278
|
+
fileName = attr.fileName;
|
|
3279
|
+
break;
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
matches.push({
|
|
3283
|
+
messageId: message.id.toString(),
|
|
3284
|
+
name: fileName ?? "file",
|
|
3285
|
+
size,
|
|
3286
|
+
mimeType: document.mimeType
|
|
3287
|
+
});
|
|
3288
|
+
}
|
|
3289
|
+
} catch (error) {
|
|
3290
|
+
throw mapTelegramError(error, "Telegram lookup failed");
|
|
3291
|
+
}
|
|
3292
|
+
return matches;
|
|
3293
|
+
}
|
|
3197
3294
|
async stat(ref) {
|
|
3198
3295
|
const message = await this.getMessage(ref.messageId);
|
|
3199
3296
|
if (!message?.media) return null;
|