@sinoia/hubdoc-tools 1.8.1 → 1.8.3
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/cli.js +237 -41
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -732518,12 +732518,57 @@ var import_chalk26 = __toESM(require_source());
|
|
|
732518
732518
|
|
|
732519
732519
|
// apps/cli/cli/fuse/hubdoc-fs.ts
|
|
732520
732520
|
var import_fs_extra32 = __toESM(require_lib());
|
|
732521
|
+
var import_fs3 = require("fs");
|
|
732521
732522
|
var import_os7 = __toESM(require("os"));
|
|
732522
732523
|
var import_path34 = __toESM(require("path"));
|
|
732523
732524
|
var import_crypto6 = __toESM(require("crypto"));
|
|
732524
732525
|
var import_form_data5 = __toESM(require_form_data());
|
|
732525
732526
|
var DIR_MODE = 16877;
|
|
732526
732527
|
var FILE_MODE = 33188;
|
|
732528
|
+
var CHUNKED_THRESHOLD2 = 50 * 1024 * 1024;
|
|
732529
|
+
var MIME_BY_EXT = {
|
|
732530
|
+
// Office 2007+
|
|
732531
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
732532
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
732533
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
732534
|
+
// Office legacy
|
|
732535
|
+
".doc": "application/msword",
|
|
732536
|
+
".xls": "application/vnd.ms-excel",
|
|
732537
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
732538
|
+
// OpenDocument
|
|
732539
|
+
".odt": "application/vnd.oasis.opendocument.text",
|
|
732540
|
+
".ods": "application/vnd.oasis.opendocument.spreadsheet",
|
|
732541
|
+
".odp": "application/vnd.oasis.opendocument.presentation",
|
|
732542
|
+
// PDF & text
|
|
732543
|
+
".pdf": "application/pdf",
|
|
732544
|
+
".txt": "text/plain",
|
|
732545
|
+
".md": "text/markdown",
|
|
732546
|
+
".csv": "text/csv",
|
|
732547
|
+
".json": "application/json",
|
|
732548
|
+
".xml": "application/xml",
|
|
732549
|
+
".html": "text/html",
|
|
732550
|
+
// Images
|
|
732551
|
+
".png": "image/png",
|
|
732552
|
+
".jpg": "image/jpeg",
|
|
732553
|
+
".jpeg": "image/jpeg",
|
|
732554
|
+
".gif": "image/gif",
|
|
732555
|
+
".webp": "image/webp",
|
|
732556
|
+
".svg": "image/svg+xml",
|
|
732557
|
+
".tiff": "image/tiff",
|
|
732558
|
+
".tif": "image/tiff",
|
|
732559
|
+
".heic": "image/heic",
|
|
732560
|
+
// Archives
|
|
732561
|
+
".zip": "application/zip",
|
|
732562
|
+
".gz": "application/gzip",
|
|
732563
|
+
".tar": "application/x-tar",
|
|
732564
|
+
".7z": "application/x-7z-compressed"
|
|
732565
|
+
};
|
|
732566
|
+
function mimeForFile(name) {
|
|
732567
|
+
return MIME_BY_EXT[import_path34.default.extname(name).toLowerCase()] || "application/octet-stream";
|
|
732568
|
+
}
|
|
732569
|
+
function isOfficeLockfile(name) {
|
|
732570
|
+
return /^~\$/.test(name) || /^\.~lock\./.test(name) || /^Word Work File /.test(name);
|
|
732571
|
+
}
|
|
732527
732572
|
var HubdocFs = class {
|
|
732528
732573
|
constructor(ctx, root, opts = {}) {
|
|
732529
732574
|
this.ctx = ctx;
|
|
@@ -732697,47 +732742,127 @@ var HubdocFs = class {
|
|
|
732697
732742
|
node.downloaded = true;
|
|
732698
732743
|
node.size = (await import_fs_extra32.default.stat(dest)).size;
|
|
732699
732744
|
}
|
|
732700
|
-
|
|
732745
|
+
/**
|
|
732746
|
+
* Upload the local cache of `node` to Hubdoc — either as a new document
|
|
732747
|
+
* (POST) or as a new version of an existing one (PATCH). Files above
|
|
732748
|
+
* `CHUNKED_THRESHOLD` go through `ChunkedUploader` so large Excel/PPT
|
|
732749
|
+
* workbooks don't stall the multipart POST.
|
|
732750
|
+
*/
|
|
732751
|
+
async uploadNode(node, mode) {
|
|
732752
|
+
if (node.localOnly) return;
|
|
732701
732753
|
const local = this.cacheFileFor(node);
|
|
732754
|
+
const size = (await import_fs_extra32.default.stat(local)).size;
|
|
732755
|
+
const contentType2 = mimeForFile(node.name);
|
|
732756
|
+
if (mode === "update" && !node.id) {
|
|
732757
|
+
mode = "create";
|
|
732758
|
+
}
|
|
732759
|
+
if (mode === "create" && size >= CHUNKED_THRESHOLD2) {
|
|
732760
|
+
const uploader = new ChunkedUploader2(this.ctx.baseUrl, this.ctx.token);
|
|
732761
|
+
const result = await uploader.uploadFile({
|
|
732762
|
+
filePath: local,
|
|
732763
|
+
workspaceId: node.workspaceId,
|
|
732764
|
+
folderId: node.folderId
|
|
732765
|
+
});
|
|
732766
|
+
if (!result.success) throw new Error(`Chunked upload failed: ${result.error || "unknown error"}`);
|
|
732767
|
+
const form2 = new import_form_data5.default();
|
|
732768
|
+
form2.append("chunked_upload_id", result.uploadId);
|
|
732769
|
+
if (node.folderId) form2.append("documents_folder_id", node.folderId);
|
|
732770
|
+
else if (node.workspaceId) form2.append("workspace_id", node.workspaceId);
|
|
732771
|
+
const res = await this.ctx.axios.post("/api/v1/documents/documents", form2, {
|
|
732772
|
+
headers: form2.getHeaders(),
|
|
732773
|
+
maxBodyLength: Infinity,
|
|
732774
|
+
maxContentLength: Infinity
|
|
732775
|
+
});
|
|
732776
|
+
const created = res.data?.document ?? res.data;
|
|
732777
|
+
if (created?.id) node.id = created.id;
|
|
732778
|
+
node.isNew = false;
|
|
732779
|
+
node.size = size;
|
|
732780
|
+
return;
|
|
732781
|
+
}
|
|
732702
732782
|
const form = new import_form_data5.default();
|
|
732703
732783
|
form.append("uploaded_file", import_fs_extra32.default.createReadStream(local), {
|
|
732704
732784
|
filename: node.name,
|
|
732705
|
-
contentType:
|
|
732785
|
+
contentType: contentType2
|
|
732706
732786
|
});
|
|
732707
|
-
if (
|
|
732708
|
-
|
|
732709
|
-
|
|
732710
|
-
|
|
732711
|
-
|
|
732712
|
-
|
|
732713
|
-
|
|
732714
|
-
|
|
732715
|
-
|
|
732716
|
-
|
|
732717
|
-
|
|
732718
|
-
|
|
732719
|
-
|
|
732720
|
-
|
|
732787
|
+
if (mode === "create") {
|
|
732788
|
+
if (node.folderId) form.append("documents_folder_id", node.folderId);
|
|
732789
|
+
else if (node.workspaceId) form.append("workspace_id", node.workspaceId);
|
|
732790
|
+
if (node.name.toLowerCase().endsWith(".md")) {
|
|
732791
|
+
this.log(`\u26A0\uFE0F ${node.name}: write-back markdown \u2014 voir caveat collab #508`);
|
|
732792
|
+
}
|
|
732793
|
+
const res = await this.ctx.axios.post("/api/v1/documents/documents", form, {
|
|
732794
|
+
headers: form.getHeaders(),
|
|
732795
|
+
maxBodyLength: Infinity,
|
|
732796
|
+
maxContentLength: Infinity
|
|
732797
|
+
});
|
|
732798
|
+
const created = res.data?.document ?? res.data;
|
|
732799
|
+
if (created?.id) node.id = created.id;
|
|
732800
|
+
node.isNew = false;
|
|
732801
|
+
} else {
|
|
732802
|
+
await this.ctx.axios.patch(`/api/v1/documents/documents/${node.id}`, form, {
|
|
732803
|
+
headers: form.getHeaders(),
|
|
732804
|
+
maxBodyLength: Infinity,
|
|
732805
|
+
maxContentLength: Infinity
|
|
732806
|
+
});
|
|
732807
|
+
}
|
|
732808
|
+
node.size = size;
|
|
732809
|
+
}
|
|
732810
|
+
async uploadNew(node) {
|
|
732811
|
+
await this.uploadNode(node, "create");
|
|
732721
732812
|
}
|
|
732722
732813
|
async uploadUpdate(node) {
|
|
732723
|
-
|
|
732724
|
-
|
|
732725
|
-
|
|
732726
|
-
|
|
732727
|
-
|
|
732728
|
-
|
|
732729
|
-
|
|
732730
|
-
|
|
732731
|
-
|
|
732732
|
-
|
|
732733
|
-
|
|
732734
|
-
|
|
732814
|
+
await this.uploadNode(node, "update");
|
|
732815
|
+
}
|
|
732816
|
+
/**
|
|
732817
|
+
* If `node` has unpushed writes, upload them now and clear the dirty flag.
|
|
732818
|
+
* Called from `flush` / `fsync` / `release` so that `close(fd)` (or an
|
|
732819
|
+
* explicit `fsync(fd)`) in userspace blocks until the server has the new
|
|
732820
|
+
* bytes — makes read-after-write via the API (from another process, or
|
|
732821
|
+
* from the same one after close()) immediately consistent.
|
|
732822
|
+
*
|
|
732823
|
+
* The FUSE contract:
|
|
732824
|
+
* - `flush` fires on every `close(2)` in userspace (once per close, even
|
|
732825
|
+
* for dup'd fds).
|
|
732826
|
+
* - `release` fires only after the last reference to the file is dropped
|
|
732827
|
+
* by the kernel — potentially long after `close(2)` returned.
|
|
732828
|
+
* So `flush` (not `release`) is where the upload has to happen for the
|
|
732829
|
+
* close→read-back sequence to be reliable.
|
|
732830
|
+
*/
|
|
732831
|
+
async flushDirty(node) {
|
|
732832
|
+
if (!node.dirty) return;
|
|
732833
|
+
if (this.opts.readOnly) return;
|
|
732834
|
+
if (node.localOnly) {
|
|
732835
|
+
node.dirty = false;
|
|
732836
|
+
node.mtime = /* @__PURE__ */ new Date();
|
|
732837
|
+
return;
|
|
732838
|
+
}
|
|
732839
|
+
if (node.isNew || !node.id) await this.uploadNew(node);
|
|
732840
|
+
else await this.uploadUpdate(node);
|
|
732841
|
+
node.dirty = false;
|
|
732842
|
+
node.mtime = /* @__PURE__ */ new Date();
|
|
732735
732843
|
}
|
|
732736
732844
|
invalidateParent(p3) {
|
|
732737
732845
|
const parent = this.nodes.get(parentPath(p3));
|
|
732738
732846
|
if (parent) parent.loadedAt = 0;
|
|
732739
732847
|
}
|
|
732740
732848
|
// --- FUSE ops ------------------------------------------------------------
|
|
732849
|
+
/**
|
|
732850
|
+
* Public accessor for the FUSE ops object. Used by the mount lifecycle
|
|
732851
|
+
* to hand the ops to the native binding, and by the test suite to drive
|
|
732852
|
+
* the filesystem without loading the native binding. Callers must first
|
|
732853
|
+
* install a compatible error-code source via `setFuseBinding()` (the
|
|
732854
|
+
* mount path does this via require('@cocalc/fuse-native')).
|
|
732855
|
+
*/
|
|
732856
|
+
getOps() {
|
|
732857
|
+
if (!this.fuse) {
|
|
732858
|
+
throw new Error("FUSE binding not set \u2014 call setFuseBinding() or mount() first");
|
|
732859
|
+
}
|
|
732860
|
+
return this.ops();
|
|
732861
|
+
}
|
|
732862
|
+
/** Inject a FUSE-like error-code source. Exposed for tests. */
|
|
732863
|
+
setFuseBinding(binding) {
|
|
732864
|
+
this.fuse = binding;
|
|
732865
|
+
}
|
|
732741
732866
|
ops() {
|
|
732742
732867
|
const Fuse = this.fuse;
|
|
732743
732868
|
const ro = !!this.opts.readOnly;
|
|
@@ -732781,7 +732906,18 @@ var HubdocFs = class {
|
|
|
732781
732906
|
if (node.kind !== "file") return cb(Fuse.EISDIR);
|
|
732782
732907
|
const write = (flags & 3) !== 0;
|
|
732783
732908
|
if (write && ro) return cb(Fuse.EACCES);
|
|
732784
|
-
|
|
732909
|
+
const truncOnOpen = (flags & import_fs3.constants.O_TRUNC) !== 0;
|
|
732910
|
+
if (truncOnOpen && !node.localOnly && !ro) {
|
|
732911
|
+
await import_fs_extra32.default.writeFile(this.cacheFileFor(node), Buffer.alloc(0));
|
|
732912
|
+
node.downloaded = true;
|
|
732913
|
+
node.dirty = true;
|
|
732914
|
+
node.size = 0;
|
|
732915
|
+
} else if (node.id && !node.localOnly) {
|
|
732916
|
+
await this.download(node);
|
|
732917
|
+
} else if (!node.cachePath || !await import_fs_extra32.default.pathExists(node.cachePath)) {
|
|
732918
|
+
await import_fs_extra32.default.writeFile(this.cacheFileFor(node), Buffer.alloc(0));
|
|
732919
|
+
node.downloaded = true;
|
|
732920
|
+
}
|
|
732785
732921
|
const localFd = await import_fs_extra32.default.open(this.cacheFileFor(node), "r+").catch(async () => import_fs_extra32.default.open(this.cacheFileFor(node), "w+"));
|
|
732786
732922
|
const fd = this.nextFd++;
|
|
732787
732923
|
this.handles.set(fd, { node, localFd, write });
|
|
@@ -732827,7 +732963,10 @@ var HubdocFs = class {
|
|
|
732827
732963
|
mtime: /* @__PURE__ */ new Date(),
|
|
732828
732964
|
isNew: true,
|
|
732829
732965
|
dirty: true,
|
|
732830
|
-
downloaded: true
|
|
732966
|
+
downloaded: true,
|
|
732967
|
+
// Office / LibreOffice lockfiles must never be pushed to the server —
|
|
732968
|
+
// they are pure kernel-side artefacts of an in-progress save.
|
|
732969
|
+
localOnly: isOfficeLockfile(name)
|
|
732831
732970
|
};
|
|
732832
732971
|
await import_fs_extra32.default.writeFile(this.cacheFileFor(node), Buffer.alloc(0));
|
|
732833
732972
|
this.nodes.set(p3, node);
|
|
@@ -732845,21 +732984,28 @@ var HubdocFs = class {
|
|
|
732845
732984
|
await import_fs_extra32.default.close(h3.localFd);
|
|
732846
732985
|
} catch {
|
|
732847
732986
|
}
|
|
732848
|
-
|
|
732849
|
-
|
|
732850
|
-
|
|
732851
|
-
|
|
732852
|
-
|
|
732853
|
-
|
|
732987
|
+
await this.flushDirty(h3.node);
|
|
732988
|
+
cb(0);
|
|
732989
|
+
}),
|
|
732990
|
+
flush: wrap(async (_p, fd, cb) => {
|
|
732991
|
+
const h3 = this.handles.get(fd);
|
|
732992
|
+
if (!h3) return cb(0);
|
|
732993
|
+
await this.flushDirty(h3.node);
|
|
732994
|
+
cb(0);
|
|
732995
|
+
}),
|
|
732996
|
+
fsync: wrap(async (_p, fd, _datasync, cb) => {
|
|
732997
|
+
const h3 = this.handles.get(fd);
|
|
732998
|
+
if (!h3) return cb(0);
|
|
732999
|
+
await this.flushDirty(h3.node);
|
|
732854
733000
|
cb(0);
|
|
732855
733001
|
}),
|
|
732856
|
-
flush: (_p, _fd, cb) => cb(0),
|
|
732857
|
-
fsync: (_p, _fd, _datasync, cb) => cb(0),
|
|
732858
733002
|
unlink: wrap(async (p3, cb) => {
|
|
732859
733003
|
if (ro) return cb(Fuse.EACCES);
|
|
732860
733004
|
const node = await this.resolve(p3);
|
|
732861
733005
|
if (!node || node.kind !== "file") return cb(Fuse.ENOENT);
|
|
732862
|
-
if (node.id
|
|
733006
|
+
if (node.id && !node.localOnly) {
|
|
733007
|
+
await this.ctx.axios.delete(`/api/v1/documents/documents/${node.id}`);
|
|
733008
|
+
}
|
|
732863
733009
|
if (node.cachePath) await import_fs_extra32.default.remove(node.cachePath).catch(() => {
|
|
732864
733010
|
});
|
|
732865
733011
|
this.nodes.delete(p3);
|
|
@@ -732907,6 +733053,56 @@ var HubdocFs = class {
|
|
|
732907
733053
|
if (!newParent || newParent.kind !== "dir" || newParent.hub === "root") return cb(Fuse.EPERM);
|
|
732908
733054
|
const newName = import_path34.default.basename(dest);
|
|
732909
733055
|
const newFolderId = newParent.hub === "folder" ? newParent.folderId : null;
|
|
733056
|
+
if (node.localOnly) {
|
|
733057
|
+
this.nodes.delete(src);
|
|
733058
|
+
node.name = newName;
|
|
733059
|
+
node.folderId = node.kind === "file" ? newFolderId ?? void 0 : node.folderId;
|
|
733060
|
+
node.workspaceId = newParent.workspaceId;
|
|
733061
|
+
this.nodes.set(dest, node);
|
|
733062
|
+
this.invalidateParent(src);
|
|
733063
|
+
this.invalidateParent(dest);
|
|
733064
|
+
return cb(0);
|
|
733065
|
+
}
|
|
733066
|
+
let destNode = null;
|
|
733067
|
+
try {
|
|
733068
|
+
destNode = await this.resolve(dest);
|
|
733069
|
+
} catch {
|
|
733070
|
+
}
|
|
733071
|
+
if (node.kind === "file" && !node.id) {
|
|
733072
|
+
if (destNode && destNode.kind === "file" && destNode.id) {
|
|
733073
|
+
const sourceCache = this.cacheFileFor(node);
|
|
733074
|
+
const local = this.cacheFileFor(destNode);
|
|
733075
|
+
await import_fs_extra32.default.copy(sourceCache, local, { overwrite: true });
|
|
733076
|
+
destNode.dirty = true;
|
|
733077
|
+
destNode.downloaded = true;
|
|
733078
|
+
destNode.name = newName;
|
|
733079
|
+
destNode.folderId = newFolderId ?? void 0;
|
|
733080
|
+
destNode.workspaceId = newParent.workspaceId;
|
|
733081
|
+
await this.uploadNode(destNode, "update");
|
|
733082
|
+
destNode.dirty = false;
|
|
733083
|
+
destNode.mtime = /* @__PURE__ */ new Date();
|
|
733084
|
+
this.nodes.delete(src);
|
|
733085
|
+
await import_fs_extra32.default.remove(sourceCache).catch(() => {
|
|
733086
|
+
});
|
|
733087
|
+
this.invalidateParent(src);
|
|
733088
|
+
this.invalidateParent(dest);
|
|
733089
|
+
return cb(0);
|
|
733090
|
+
}
|
|
733091
|
+
node.name = newName;
|
|
733092
|
+
node.folderId = newFolderId ?? void 0;
|
|
733093
|
+
node.workspaceId = newParent.workspaceId;
|
|
733094
|
+
await this.uploadNew(node);
|
|
733095
|
+
this.nodes.delete(src);
|
|
733096
|
+
this.nodes.set(dest, node);
|
|
733097
|
+
this.invalidateParent(src);
|
|
733098
|
+
this.invalidateParent(dest);
|
|
733099
|
+
return cb(0);
|
|
733100
|
+
}
|
|
733101
|
+
if (destNode && destNode !== node && destNode.kind === "file" && destNode.id) {
|
|
733102
|
+
await this.ctx.axios.delete(`/api/v1/documents/documents/${destNode.id}`).catch(() => {
|
|
733103
|
+
});
|
|
733104
|
+
this.nodes.delete(dest);
|
|
733105
|
+
}
|
|
732910
733106
|
const resource = node.kind === "file" ? "documents" : "folders";
|
|
732911
733107
|
if (!node.id) return cb(Fuse.EPERM);
|
|
732912
733108
|
await this.ctx.axios.patch(`/api/v1/documents/${resource}/${node.id}`, {
|
|
@@ -732940,7 +733136,7 @@ var HubdocFs = class {
|
|
|
732940
733136
|
}
|
|
732941
733137
|
this.fuse = Fuse;
|
|
732942
733138
|
await import_fs_extra32.default.ensureDir(mountPath);
|
|
732943
|
-
const fuse = new Fuse(mountPath, this.
|
|
733139
|
+
const fuse = new Fuse(mountPath, this.getOps(), {
|
|
732944
733140
|
force: true,
|
|
732945
733141
|
mkdir: true,
|
|
732946
733142
|
displayFolder: "Hubdoc",
|
|
@@ -733760,7 +733956,7 @@ async function handlePermissionsShow(id, opts) {
|
|
|
733760
733956
|
// apps/cli/cli.ts
|
|
733761
733957
|
var getVersion = () => {
|
|
733762
733958
|
try {
|
|
733763
|
-
if (true) return "1.8.
|
|
733959
|
+
if (true) return "1.8.3";
|
|
733764
733960
|
} catch {
|
|
733765
733961
|
}
|
|
733766
733962
|
for (const candidate of [
|