mirror of
https://github.com/remotely-save/remotely-save.git
synced 2024-06-07 21:10:45 +00:00
allowing s3 synth folder
This commit is contained in:
parent
a9126e5947
commit
df7b6e1848
@ -29,6 +29,8 @@ export interface S3Config {
|
|||||||
useAccurateMTime?: boolean;
|
useAccurateMTime?: boolean;
|
||||||
reverseProxyNoSignUrl?: string;
|
reverseProxyNoSignUrl?: string;
|
||||||
|
|
||||||
|
generateFolderObject?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
|
@ -211,6 +211,7 @@ export class FakeFsEncrypt extends FakeFs {
|
|||||||
sizeEnc: innerEntity.size!,
|
sizeEnc: innerEntity.size!,
|
||||||
sizeRaw: innerEntity.sizeRaw,
|
sizeRaw: innerEntity.sizeRaw,
|
||||||
hash: undefined,
|
hash: undefined,
|
||||||
|
synthesizedFolder: innerEntity.synthesizedFolder,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.cacheMapOrigToEnc[key] = innerEntity.keyRaw;
|
this.cacheMapOrigToEnc[key] = innerEntity.keyRaw;
|
||||||
@ -243,6 +244,7 @@ export class FakeFsEncrypt extends FakeFs {
|
|||||||
sizeEnc: innerEntity.size!,
|
sizeEnc: innerEntity.size!,
|
||||||
sizeRaw: innerEntity.sizeRaw,
|
sizeRaw: innerEntity.sizeRaw,
|
||||||
hash: undefined,
|
hash: undefined,
|
||||||
|
synthesizedFolder: innerEntity.synthesizedFolder,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -287,6 +289,7 @@ export class FakeFsEncrypt extends FakeFs {
|
|||||||
sizeEnc: innerEntity.size!,
|
sizeEnc: innerEntity.size!,
|
||||||
sizeRaw: innerEntity.sizeRaw,
|
sizeRaw: innerEntity.sizeRaw,
|
||||||
hash: undefined,
|
hash: undefined,
|
||||||
|
synthesizedFolder: innerEntity.synthesizedFolder,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -336,6 +339,7 @@ export class FakeFsEncrypt extends FakeFs {
|
|||||||
sizeEnc: innerEntity.size!,
|
sizeEnc: innerEntity.size!,
|
||||||
sizeRaw: innerEntity.sizeRaw,
|
sizeRaw: innerEntity.sizeRaw,
|
||||||
hash: undefined,
|
hash: undefined,
|
||||||
|
synthesizedFolder: innerEntity.synthesizedFolder,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
73
src/fsS3.ts
73
src/fsS3.ts
@ -26,7 +26,7 @@ import { Readable } from "stream";
|
|||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import AggregateError from "aggregate-error";
|
import AggregateError from "aggregate-error";
|
||||||
import { DEFAULT_CONTENT_TYPE, S3Config, VALID_REQURL } from "./baseTypes";
|
import { DEFAULT_CONTENT_TYPE, S3Config, VALID_REQURL } from "./baseTypes";
|
||||||
import { bufferToArrayBuffer } from "./misc";
|
import { bufferToArrayBuffer, getFolderLevels } from "./misc";
|
||||||
import PQueue from "p-queue";
|
import PQueue from "p-queue";
|
||||||
|
|
||||||
import { Entity } from "./baseTypes";
|
import { Entity } from "./baseTypes";
|
||||||
@ -186,6 +186,7 @@ export const DEFAULT_S3_CONFIG: S3Config = {
|
|||||||
remotePrefix: "",
|
remotePrefix: "",
|
||||||
useAccurateMTime: false, // it causes money, disable by default
|
useAccurateMTime: false, // it causes money, disable by default
|
||||||
reverseProxyNoSignUrl: "",
|
reverseProxyNoSignUrl: "",
|
||||||
|
generateFolderObject: false, // new version, by default not generate folders
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -385,11 +386,13 @@ export class FakeFsS3 extends FakeFs {
|
|||||||
s3Config: S3Config;
|
s3Config: S3Config;
|
||||||
s3Client: S3Client;
|
s3Client: S3Client;
|
||||||
kind: "s3";
|
kind: "s3";
|
||||||
|
synthFoldersCache: Record<string, Entity>;
|
||||||
constructor(s3Config: S3Config) {
|
constructor(s3Config: S3Config) {
|
||||||
super();
|
super();
|
||||||
this.s3Config = s3Config;
|
this.s3Config = s3Config;
|
||||||
this.s3Client = getS3Client(s3Config);
|
this.s3Client = getS3Client(s3Config);
|
||||||
this.kind = "s3";
|
this.kind = "s3";
|
||||||
|
this.synthFoldersCache = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
async walk(): Promise<Entity[]> {
|
async walk(): Promise<Entity[]> {
|
||||||
@ -484,17 +487,52 @@ export class FakeFsS3 extends FakeFs {
|
|||||||
// ensemble fake rsp
|
// ensemble fake rsp
|
||||||
// in the end, we need to transform the response list
|
// in the end, we need to transform the response list
|
||||||
// back to the local contents-alike list
|
// back to the local contents-alike list
|
||||||
return contents.map((x) =>
|
const res: Entity[] = [];
|
||||||
fromS3ObjectToEntity(
|
const realEnrities = new Set<string>();
|
||||||
x,
|
for (const remoteObj of contents) {
|
||||||
|
const remoteEntity = fromS3ObjectToEntity(
|
||||||
|
remoteObj,
|
||||||
this.s3Config.remotePrefix ?? "",
|
this.s3Config.remotePrefix ?? "",
|
||||||
mtimeRecords,
|
mtimeRecords,
|
||||||
ctimeRecords
|
ctimeRecords
|
||||||
)
|
);
|
||||||
);
|
realEnrities.add(remoteEntity.key!);
|
||||||
|
res.push(remoteEntity);
|
||||||
|
|
||||||
|
for (const f of getFolderLevels(remoteEntity.key!, true)) {
|
||||||
|
if (realEnrities.has(f)) {
|
||||||
|
delete this.synthFoldersCache[f];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!this.synthFoldersCache.hasOwnProperty(f) ||
|
||||||
|
remoteEntity.mtimeSvr! >= this.synthFoldersCache[f].mtimeSvr!
|
||||||
|
) {
|
||||||
|
this.synthFoldersCache[f] = {
|
||||||
|
key: f,
|
||||||
|
keyRaw: f,
|
||||||
|
size: 0,
|
||||||
|
sizeRaw: 0,
|
||||||
|
sizeEnc: 0,
|
||||||
|
mtimeSvr: remoteEntity.mtimeSvr,
|
||||||
|
mtimeSvrFmt: remoteEntity.mtimeSvrFmt,
|
||||||
|
mtimeCli: remoteEntity.mtimeCli,
|
||||||
|
mtimeCliFmt: remoteEntity.mtimeCliFmt,
|
||||||
|
synthesizedFolder: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const key of Object.keys(this.synthFoldersCache)) {
|
||||||
|
res.push(this.synthFoldersCache[key]);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
async stat(key: string): Promise<Entity> {
|
async stat(key: string): Promise<Entity> {
|
||||||
|
if (this.synthFoldersCache.hasOwnProperty(key)) {
|
||||||
|
return this.synthFoldersCache[key];
|
||||||
|
}
|
||||||
let keyFullPath = key;
|
let keyFullPath = key;
|
||||||
keyFullPath = getRemoteWithPrefixPath(
|
keyFullPath = getRemoteWithPrefixPath(
|
||||||
keyFullPath,
|
keyFullPath,
|
||||||
@ -529,6 +567,23 @@ export class FakeFsS3 extends FakeFs {
|
|||||||
if (!key.endsWith("/")) {
|
if (!key.endsWith("/")) {
|
||||||
throw new Error(`You should not call mkdir on ${key}!`);
|
throw new Error(`You should not call mkdir on ${key}!`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const generateFolderObject = this.s3Config.generateFolderObject ?? false;
|
||||||
|
if (!generateFolderObject) {
|
||||||
|
const synth = {
|
||||||
|
key: key,
|
||||||
|
keyRaw: key,
|
||||||
|
size: 0,
|
||||||
|
sizeRaw: 0,
|
||||||
|
sizeEnc: 0,
|
||||||
|
mtimeSvr: mtime,
|
||||||
|
mtimeCli: mtime,
|
||||||
|
synthesizedFolder: true,
|
||||||
|
};
|
||||||
|
this.synthFoldersCache[key] = synth;
|
||||||
|
return synth;
|
||||||
|
}
|
||||||
|
|
||||||
const uploadFile = getRemoteWithPrefixPath(
|
const uploadFile = getRemoteWithPrefixPath(
|
||||||
key,
|
key,
|
||||||
this.s3Config.remotePrefix ?? ""
|
this.s3Config.remotePrefix ?? ""
|
||||||
@ -670,6 +725,12 @@ export class FakeFsS3 extends FakeFs {
|
|||||||
if (key === "/") {
|
if (key === "/") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.synthFoldersCache.hasOwnProperty(key)) {
|
||||||
|
delete this.synthFoldersCache[key];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const remoteFileName = getRemoteWithPrefixPath(
|
const remoteFileName = getRemoteWithPrefixPath(
|
||||||
key,
|
key,
|
||||||
this.s3Config.remotePrefix ?? ""
|
this.s3Config.remotePrefix ?? ""
|
||||||
|
@ -183,6 +183,10 @@
|
|||||||
"settings_s3_urlstyle_desc": "Whether to force path-style URLs for S3 objects (e.g., https://s3.amazonaws.com/*/ instead of https://*.s3.amazonaws.com/).",
|
"settings_s3_urlstyle_desc": "Whether to force path-style URLs for S3 objects (e.g., https://s3.amazonaws.com/*/ instead of https://*.s3.amazonaws.com/).",
|
||||||
"settings_s3_reverse_proxy_no_sign_url": "S3 Reverse Proxy (No Sign) Url (experimental)",
|
"settings_s3_reverse_proxy_no_sign_url": "S3 Reverse Proxy (No Sign) Url (experimental)",
|
||||||
"settings_s3_reverse_proxy_no_sign_url_desc": "S3 reverse proxy url without signature. This is useful if you use a revers proxy but do not change the original credential signature. No http(s):// prefix. Leave it blank if you don't know what it is.",
|
"settings_s3_reverse_proxy_no_sign_url_desc": "S3 reverse proxy url without signature. This is useful if you use a revers proxy but do not change the original credential signature. No http(s):// prefix. Leave it blank if you don't know what it is.",
|
||||||
|
"settings_s3_generatefolderobject": "Generate Folder Object Or Not",
|
||||||
|
"settings_s3_generatefolderobject_desc": "S3 doesn't have \"real\" folder. If you set \"Generate\" here (or use old version), the plugin will upload a zero-byte object endding with \"/\" to represent the folder. In the new version, the plugin skips generating folder object by default.",
|
||||||
|
"settings_s3_generatefolderobject_notgenerate": "Not generate (default)",
|
||||||
|
"settings_s3_generatefolderobject_generate": "Generate",
|
||||||
"settings_s3_connect_succ": "Great! The bucket can be accessed.",
|
"settings_s3_connect_succ": "Great! The bucket can be accessed.",
|
||||||
"settings_s3_connect_fail": "The S3 bucket cannot be reached.",
|
"settings_s3_connect_fail": "The S3 bucket cannot be reached.",
|
||||||
"settings_dropbox": "Remote For Dropbox",
|
"settings_dropbox": "Remote For Dropbox",
|
||||||
|
@ -182,6 +182,10 @@
|
|||||||
"settings_s3_urlstyle_desc": "是否对 S3 对象强制使用 path style URL(例如使用 https://s3.amazonaws.com/*/ 而不是 https://*.s3.amazonaws.com/)。",
|
"settings_s3_urlstyle_desc": "是否对 S3 对象强制使用 path style URL(例如使用 https://s3.amazonaws.com/*/ 而不是 https://*.s3.amazonaws.com/)。",
|
||||||
"settings_s3_reverse_proxy_no_sign_url": "S3 反向代理(不签名)地址(实验性质)",
|
"settings_s3_reverse_proxy_no_sign_url": "S3 反向代理(不签名)地址(实验性质)",
|
||||||
"settings_s3_reverse_proxy_no_sign_url_desc": "不会参与到签名的 S3 反向代理地址。如果您有一个反向代理,但是不想修改原始鉴权签名,这里就可以填写。没有 http(s):// 前缀。如果您不知道这是什么,留空即可。",
|
"settings_s3_reverse_proxy_no_sign_url_desc": "不会参与到签名的 S3 反向代理地址。如果您有一个反向代理,但是不想修改原始鉴权签名,这里就可以填写。没有 http(s):// 前缀。如果您不知道这是什么,留空即可。",
|
||||||
|
"settings_s3_generatefolderobject": "是否生成文件夹 Object",
|
||||||
|
"settings_s3_generatefolderobject_desc": "S3 不存在“真正”的文件夹。如果您设置了“生成”(或用了旧版本),那么插件会上传 0 字节的以“/”结尾的 Object 来代表文件夹。新版本插件会默认跳过生成这种文件夹 Object。",
|
||||||
|
"settings_s3_generatefolderobject_notgenerate": "不生成(默认)",
|
||||||
|
"settings_s3_generatefolderobject_generate": "生成",
|
||||||
"settings_s3_connect_succ": "很好!可以访问到对应存储桶。",
|
"settings_s3_connect_succ": "很好!可以访问到对应存储桶。",
|
||||||
"settings_s3_connect_fail": "无法访问到对应存储桶。",
|
"settings_s3_connect_fail": "无法访问到对应存储桶。",
|
||||||
"settings_dropbox": "Dropbox 设置",
|
"settings_dropbox": "Dropbox 设置",
|
||||||
|
@ -181,6 +181,10 @@
|
|||||||
"settings_s3_urlstyle_desc": "是否對 S3 物件強制使用 path style URL(例如使用 https://s3.amazonaws.com/*/ 而不是 https://*.s3.amazonaws.com/)。",
|
"settings_s3_urlstyle_desc": "是否對 S3 物件強制使用 path style URL(例如使用 https://s3.amazonaws.com/*/ 而不是 https://*.s3.amazonaws.com/)。",
|
||||||
"settings_s3_reverse_proxy_no_sign_url": "S3 反向代理(不簽名)地址(實驗性質)",
|
"settings_s3_reverse_proxy_no_sign_url": "S3 反向代理(不簽名)地址(實驗性質)",
|
||||||
"settings_s3_reverse_proxy_no_sign_url_desc": "不會參與到簽名的 S3 反向代理地址。如果您有一個反向代理,但是不想修改原始鑑權簽名,這裡就可以填寫。沒有 http(s):// 字首。如果您不知道這是什麼,留空即可。",
|
"settings_s3_reverse_proxy_no_sign_url_desc": "不會參與到簽名的 S3 反向代理地址。如果您有一個反向代理,但是不想修改原始鑑權簽名,這裡就可以填寫。沒有 http(s):// 字首。如果您不知道這是什麼,留空即可。",
|
||||||
|
"settings_s3_generatefolderobject": "是否生成文件夾 Object",
|
||||||
|
"settings_s3_generatefolderobject_desc": "S3 不存在“真正”的文件夾。如果您設置了“生成”(或用了舊版本),那麼插件會上傳 0 字節的以“/”結尾的 Object 來代表文件夾。新版本插件會默認跳過生成這種文件夾 Object。",
|
||||||
|
"settings_s3_generatefolderobject_notgenerate": "不生成(默認)",
|
||||||
|
"settings_s3_generatefolderobject_generate": "生成",
|
||||||
"settings_s3_connect_succ": "很好!可以訪問到對應儲存桶。",
|
"settings_s3_connect_succ": "很好!可以訪問到對應儲存桶。",
|
||||||
"settings_s3_connect_fail": "無法訪問到對應儲存桶。",
|
"settings_s3_connect_fail": "無法訪問到對應儲存桶。",
|
||||||
"settings_dropbox": "Dropbox 設定",
|
"settings_dropbox": "Dropbox 設定",
|
||||||
|
@ -868,6 +868,9 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
// it causes money, so disable it by default
|
// it causes money, so disable it by default
|
||||||
this.settings.s3.useAccurateMTime = false;
|
this.settings.s3.useAccurateMTime = false;
|
||||||
}
|
}
|
||||||
|
if (this.settings.s3.generateFolderObject === undefined) {
|
||||||
|
this.settings.s3.generateFolderObject = false;
|
||||||
|
}
|
||||||
if (this.settings.ignorePaths === undefined) {
|
if (this.settings.ignorePaths === undefined) {
|
||||||
this.settings.ignorePaths = [];
|
this.settings.ignorePaths = [];
|
||||||
}
|
}
|
||||||
|
@ -1067,6 +1067,34 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
new Setting(s3Div)
|
||||||
|
.setName(t("settings_s3_generatefolderobject"))
|
||||||
|
.setDesc(t("settings_s3_generatefolderobject_desc"))
|
||||||
|
.addDropdown((dropdown) => {
|
||||||
|
dropdown
|
||||||
|
.addOption(
|
||||||
|
"notgenerate",
|
||||||
|
t("settings_s3_generatefolderobject_notgenerate")
|
||||||
|
)
|
||||||
|
.addOption(
|
||||||
|
"generate",
|
||||||
|
t("settings_s3_generatefolderobject_generate")
|
||||||
|
);
|
||||||
|
|
||||||
|
dropdown
|
||||||
|
.setValue(
|
||||||
|
`${this.plugin.settings.s3.generateFolderObject ? "generate" : "notgenerate"}`
|
||||||
|
)
|
||||||
|
.onChange(async (val) => {
|
||||||
|
if (val === "generate") {
|
||||||
|
this.plugin.settings.s3.generateFolderObject = true;
|
||||||
|
} else {
|
||||||
|
this.plugin.settings.s3.generateFolderObject = false;
|
||||||
|
}
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
new Setting(s3Div)
|
new Setting(s3Div)
|
||||||
.setName(t("settings_checkonnectivity"))
|
.setName(t("settings_checkonnectivity"))
|
||||||
.setDesc(t("settings_checkonnectivity_desc"))
|
.setDesc(t("settings_checkonnectivity_desc"))
|
||||||
|
42
src/sync.ts
42
src/sync.ts
@ -20,7 +20,6 @@ import {
|
|||||||
} from "./localdb";
|
} from "./localdb";
|
||||||
import {
|
import {
|
||||||
atWhichLevel,
|
atWhichLevel,
|
||||||
getFolderLevels,
|
|
||||||
getParentFolder,
|
getParentFolder,
|
||||||
isHiddenPath,
|
isHiddenPath,
|
||||||
isSpecialFolderNameToSkip,
|
isSpecialFolderNameToSkip,
|
||||||
@ -161,10 +160,7 @@ const ensembleMixedEnties = async (
|
|||||||
|
|
||||||
const finalMappings: SyncPlanType = {};
|
const finalMappings: SyncPlanType = {};
|
||||||
|
|
||||||
const synthFolders: Record<string, Entity> = {};
|
|
||||||
|
|
||||||
// remote has to be first
|
// remote has to be first
|
||||||
// we also have to synthesize folders here
|
|
||||||
for (const remote of remoteEntityList) {
|
for (const remote of remoteEntityList) {
|
||||||
const remoteCopied = ensureMTimeOfRemoteEntityValid(
|
const remoteCopied = ensureMTimeOfRemoteEntityValid(
|
||||||
copyEntityAndFixTimeFormat(remote, serviceType)
|
copyEntityAndFixTimeFormat(remote, serviceType)
|
||||||
@ -187,48 +183,10 @@ const ensembleMixedEnties = async (
|
|||||||
key: key,
|
key: key,
|
||||||
remote: remoteCopied,
|
remote: remoteCopied,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const f of getFolderLevels(key, true)) {
|
|
||||||
if (finalMappings.hasOwnProperty(f)) {
|
|
||||||
delete synthFolders[f];
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!synthFolders.hasOwnProperty(f) ||
|
|
||||||
remoteCopied.mtimeSvr! >= synthFolders[f].mtimeSvr!
|
|
||||||
) {
|
|
||||||
synthFolders[f] = {
|
|
||||||
key: f,
|
|
||||||
keyRaw: `<synth: ${f}>`,
|
|
||||||
keyEnc: `<enc synth: ${f}>`,
|
|
||||||
size: 0,
|
|
||||||
sizeRaw: 0,
|
|
||||||
sizeEnc: 0,
|
|
||||||
mtimeSvr: remoteCopied.mtimeSvr,
|
|
||||||
mtimeSvrFmt: remoteCopied.mtimeSvrFmt,
|
|
||||||
mtimeCli: remoteCopied.mtimeCli,
|
|
||||||
mtimeCliFmt: remoteCopied.mtimeCliFmt,
|
|
||||||
synthesizedFolder: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
profiler.insert("ensembleMixedEnties: finish remote");
|
profiler.insert("ensembleMixedEnties: finish remote");
|
||||||
|
|
||||||
console.debug(`synthFolders:`);
|
|
||||||
console.debug(synthFolders);
|
|
||||||
|
|
||||||
// special: add synth folders
|
|
||||||
for (const key of Object.keys(synthFolders)) {
|
|
||||||
finalMappings[key] = {
|
|
||||||
key: key,
|
|
||||||
remote: synthFolders[key],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
profiler.insert("ensembleMixedEnties: finish synth");
|
|
||||||
|
|
||||||
if (Object.keys(finalMappings).length === 0 || localEntityList.length === 0) {
|
if (Object.keys(finalMappings).length === 0 || localEntityList.length === 0) {
|
||||||
// Special checking:
|
// Special checking:
|
||||||
// if one side is totally empty,
|
// if one side is totally empty,
|
||||||
|
Loading…
Reference in New Issue
Block a user