@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.
Files changed (2) hide show
  1. package/cli.js +237 -41
  2. 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
- async uploadNew(node) {
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: "application/octet-stream"
732785
+ contentType: contentType2
732706
732786
  });
732707
- if (node.folderId) form.append("documents_folder_id", node.folderId);
732708
- else if (node.workspaceId) form.append("workspace_id", node.workspaceId);
732709
- if (node.name.toLowerCase().endsWith(".md")) {
732710
- this.log(`\u26A0\uFE0F ${node.name}: write-back markdown \u2014 voir caveat collab #508`);
732711
- }
732712
- const res = await this.ctx.axios.post("/api/v1/documents/documents", form, {
732713
- headers: form.getHeaders(),
732714
- maxBodyLength: Infinity,
732715
- maxContentLength: Infinity
732716
- });
732717
- const created = res.data?.document ?? res.data;
732718
- if (created?.id) node.id = created.id;
732719
- node.isNew = false;
732720
- node.size = (await import_fs_extra32.default.stat(local)).size;
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
- const local = this.cacheFileFor(node);
732724
- const form = new import_form_data5.default();
732725
- form.append("uploaded_file", import_fs_extra32.default.createReadStream(local), {
732726
- filename: node.name,
732727
- contentType: "application/octet-stream"
732728
- });
732729
- await this.ctx.axios.patch(`/api/v1/documents/documents/${node.id}`, form, {
732730
- headers: form.getHeaders(),
732731
- maxBodyLength: Infinity,
732732
- maxContentLength: Infinity
732733
- });
732734
- node.size = (await import_fs_extra32.default.stat(local)).size;
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
- if (node.id) await this.download(node);
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
- if (h3.node.dirty && !ro) {
732849
- if (h3.node.isNew || !h3.node.id) await this.uploadNew(h3.node);
732850
- else await this.uploadUpdate(h3.node);
732851
- h3.node.dirty = false;
732852
- h3.node.mtime = /* @__PURE__ */ new Date();
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) await this.ctx.axios.delete(`/api/v1/documents/documents/${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.ops(), {
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.1";
733959
+ if (true) return "1.8.3";
733764
733960
  } catch {
733765
733961
  }
733766
733962
  for (const candidate of [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sinoia/hubdoc-tools",
3
- "version": "1.8.1",
3
+ "version": "1.8.3",
4
4
  "description": "Professional command-line tool for HubDoc document management and bulk import/export",
5
5
  "main": "cli.js",
6
6
  "bin": {