This commit is contained in:
fyears
2024-01-13 19:27:51 +08:00
parent 897e4fc4e5
commit 29a3c76b4e
6 changed files with 148 additions and 10 deletions
+113 -10
View File
@@ -42,6 +42,7 @@ import {
export { S3Client } from "@aws-sdk/client-s3";
import { log } from "./moreOnLog";
import PQueue from "p-queue";
////////////////////////////////////////////////////////////////////////////////
// special handler using Obsidian requestUrl
@@ -155,7 +156,7 @@ class ObsHttpHandler extends FetchHttpHandler {
// other stuffs
////////////////////////////////////////////////////////////////////////////////
export const DEFAULT_S3_CONFIG = {
export const DEFAULT_S3_CONFIG: S3Config = {
s3Endpoint: "",
s3Region: "",
s3AccessKeyID: "",
@@ -165,6 +166,7 @@ export const DEFAULT_S3_CONFIG = {
partsConcurrency: 20,
forcePathStyle: false,
remotePrefix: "",
useAccurateMTime: false, // it causes money, disable by default
};
export type S3ObjectType = _Object;
@@ -218,24 +220,47 @@ const getLocalNoPrefixPath = (
return fileOrFolderPathWithRemotePrefix.slice(`${remotePrefix}`.length);
};
const fromS3ObjectToRemoteItem = (x: S3ObjectType, remotePrefix: string) => {
return {
const fromS3ObjectToRemoteItem = (
x: S3ObjectType,
remotePrefix: string,
mtimeRecords: Record<string, number>,
ctimeRecords: Record<string, number>
) => {
let mtime = x.LastModified.valueOf();
if (x.Key in mtimeRecords) {
const m2 = mtimeRecords[x.Key];
if (m2 !== 0) {
mtime = m2;
}
}
const r: RemoteItem = {
key: getLocalNoPrefixPath(x.Key, remotePrefix),
lastModified: x.LastModified.valueOf(),
lastModified: mtime,
size: x.Size,
remoteType: "s3",
etag: x.ETag,
} as RemoteItem;
};
return r;
};
const fromS3HeadObjectToRemoteItem = (
fileOrFolderPathWithRemotePrefix: string,
x: HeadObjectCommandOutput,
remotePrefix: string
remotePrefix: string,
useAccurateMTime: boolean
) => {
let mtime = x.LastModified.valueOf();
if (useAccurateMTime && x.Metadata !== undefined) {
const m2 = Math.round(
parseFloat(x.Metadata.mtime || x.Metadata.MTime || "0")
);
if (m2 !== 0) {
mtime = m2;
}
}
return {
key: getLocalNoPrefixPath(fileOrFolderPathWithRemotePrefix, remotePrefix),
lastModified: x.LastModified.valueOf(),
lastModified: mtime,
size: x.ContentLength,
remoteType: "s3",
etag: x.ETag,
@@ -307,7 +332,8 @@ export const getRemoteMeta = async (
return fromS3HeadObjectToRemoteItem(
fileOrFolderPathWithRemotePrefix,
res,
s3Config.remotePrefix
s3Config.remotePrefix,
s3Config.useAccurateMTime
);
};
@@ -320,7 +346,9 @@ export const uploadToRemote = async (
password: string = "",
remoteEncryptedKey: string = "",
uploadRaw: boolean = false,
rawContent: string | ArrayBuffer = ""
rawContent: string | ArrayBuffer = "",
rawContentMTime: number = 0,
rawContentCTime: number = 0
) => {
let uploadFile = fileOrFolderPath;
if (password !== "") {
@@ -336,6 +364,13 @@ export const uploadToRemote = async (
throw Error(`you specify uploadRaw, but you also provide a folder key!`);
}
// folder
let mtime = 0;
let ctime = 0;
const s = await vault.adapter.stat(fileOrFolderPath);
if (s !== null) {
mtime = s.mtime;
ctime = s.ctime;
}
const contentType = DEFAULT_CONTENT_TYPE;
await s3Client.send(
new PutObjectCommand({
@@ -343,6 +378,10 @@ export const uploadToRemote = async (
Key: uploadFile,
Body: "",
ContentType: contentType,
Metadata: {
MTime: `${mtime}`,
CTime: `${ctime}`,
},
})
);
return await getRemoteMeta(s3Client, s3Config, uploadFile);
@@ -357,14 +396,23 @@ export const uploadToRemote = async (
) || DEFAULT_CONTENT_TYPE;
}
let localContent = undefined;
let mtime = 0;
let ctime = 0;
if (uploadRaw) {
if (typeof rawContent === "string") {
localContent = new TextEncoder().encode(rawContent).buffer;
} else {
localContent = rawContent;
}
mtime = rawContentMTime;
ctime = rawContentCTime;
} else {
localContent = await vault.adapter.readBinary(fileOrFolderPath);
const s = await vault.adapter.stat(fileOrFolderPath);
if (s !== null) {
mtime = s.mtime;
ctime = s.ctime;
}
}
let remoteContent = localContent;
if (password !== "") {
@@ -373,6 +421,7 @@ export const uploadToRemote = async (
const bytesIn5MB = 5242880;
const body = new Uint8Array(remoteContent);
const upload = new Upload({
client: s3Client,
queueSize: s3Config.partsConcurrency, // concurrency
@@ -383,6 +432,10 @@ export const uploadToRemote = async (
Key: uploadFile,
Body: body,
ContentType: contentType,
Metadata: {
MTime: `${mtime}`,
CTime: `${ctime}`,
},
},
});
upload.on("httpUploadProgress", (progress) => {
@@ -407,6 +460,17 @@ const listFromRemoteRaw = async (
}
const contents = [] as _Object[];
const mtimeRecords: Record<string, number> = {};
const ctimeRecords: Record<string, number> = {};
const queueHead = new PQueue({
concurrency: s3Config.partsConcurrency,
autoStart: true,
});
queueHead.on("error", (error) => {
queueHead.pause();
queueHead.clear();
throw error;
});
let isTruncated = true;
do {
@@ -420,6 +484,37 @@ const listFromRemoteRaw = async (
}
contents.push(...rsp.Contents);
if (s3Config.useAccurateMTime) {
// head requests of all objects, love it
for (const content of rsp.Contents) {
queueHead.add(async () => {
const rspHead = await s3Client.send(
new HeadObjectCommand({
Bucket: s3Config.s3BucketName,
Key: content.Key,
})
);
if (rspHead.$metadata.httpStatusCode !== 200) {
throw Error("some thing bad while heading single object!");
}
if (rspHead.Metadata === undefined) {
// pass
} else {
mtimeRecords[content.Key] = Math.round(
parseFloat(
rspHead.Metadata.mtime || rspHead.Metadata.MTime || "0"
)
);
ctimeRecords[content.Key] = Math.round(
parseFloat(
rspHead.Metadata.ctime || rspHead.Metadata.CTime || "0"
)
);
}
});
}
}
isTruncated = rsp.IsTruncated;
confCmd.ContinuationToken = rsp.NextContinuationToken;
if (
@@ -431,12 +526,20 @@ const listFromRemoteRaw = async (
}
} while (isTruncated);
// wait for any head requests
await queueHead.onIdle();
// ensemble fake rsp
// in the end, we need to transform the response list
// back to the local contents-alike list
return {
Contents: contents.map((x) =>
fromS3ObjectToRemoteItem(x, s3Config.remotePrefix)
fromS3ObjectToRemoteItem(
x,
s3Config.remotePrefix,
mtimeRecords,
ctimeRecords
)
),
};
};