@sinoia/hubdoc-tools 1.8.1 → 1.8.2

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 +200 -34
  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,99 @@ 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");
732735
732815
  }
732736
732816
  invalidateParent(p3) {
732737
732817
  const parent = this.nodes.get(parentPath(p3));
732738
732818
  if (parent) parent.loadedAt = 0;
732739
732819
  }
732740
732820
  // --- FUSE ops ------------------------------------------------------------
732821
+ /**
732822
+ * Public accessor for the FUSE ops object. Used by the mount lifecycle
732823
+ * to hand the ops to the native binding, and by the test suite to drive
732824
+ * the filesystem without loading the native binding. Callers must first
732825
+ * install a compatible error-code source via `setFuseBinding()` (the
732826
+ * mount path does this via require('@cocalc/fuse-native')).
732827
+ */
732828
+ getOps() {
732829
+ if (!this.fuse) {
732830
+ throw new Error("FUSE binding not set \u2014 call setFuseBinding() or mount() first");
732831
+ }
732832
+ return this.ops();
732833
+ }
732834
+ /** Inject a FUSE-like error-code source. Exposed for tests. */
732835
+ setFuseBinding(binding) {
732836
+ this.fuse = binding;
732837
+ }
732741
732838
  ops() {
732742
732839
  const Fuse = this.fuse;
732743
732840
  const ro = !!this.opts.readOnly;
@@ -732781,7 +732878,18 @@ var HubdocFs = class {
732781
732878
  if (node.kind !== "file") return cb(Fuse.EISDIR);
732782
732879
  const write = (flags & 3) !== 0;
732783
732880
  if (write && ro) return cb(Fuse.EACCES);
732784
- if (node.id) await this.download(node);
732881
+ const truncOnOpen = (flags & import_fs3.constants.O_TRUNC) !== 0;
732882
+ if (truncOnOpen && !node.localOnly && !ro) {
732883
+ await import_fs_extra32.default.writeFile(this.cacheFileFor(node), Buffer.alloc(0));
732884
+ node.downloaded = true;
732885
+ node.dirty = true;
732886
+ node.size = 0;
732887
+ } else if (node.id && !node.localOnly) {
732888
+ await this.download(node);
732889
+ } else if (!node.cachePath || !await import_fs_extra32.default.pathExists(node.cachePath)) {
732890
+ await import_fs_extra32.default.writeFile(this.cacheFileFor(node), Buffer.alloc(0));
732891
+ node.downloaded = true;
732892
+ }
732785
732893
  const localFd = await import_fs_extra32.default.open(this.cacheFileFor(node), "r+").catch(async () => import_fs_extra32.default.open(this.cacheFileFor(node), "w+"));
732786
732894
  const fd = this.nextFd++;
732787
732895
  this.handles.set(fd, { node, localFd, write });
@@ -732827,7 +732935,10 @@ var HubdocFs = class {
732827
732935
  mtime: /* @__PURE__ */ new Date(),
732828
732936
  isNew: true,
732829
732937
  dirty: true,
732830
- downloaded: true
732938
+ downloaded: true,
732939
+ // Office / LibreOffice lockfiles must never be pushed to the server —
732940
+ // they are pure kernel-side artefacts of an in-progress save.
732941
+ localOnly: isOfficeLockfile(name)
732831
732942
  };
732832
732943
  await import_fs_extra32.default.writeFile(this.cacheFileFor(node), Buffer.alloc(0));
732833
732944
  this.nodes.set(p3, node);
@@ -732845,11 +732956,14 @@ var HubdocFs = class {
732845
732956
  await import_fs_extra32.default.close(h3.localFd);
732846
732957
  } catch {
732847
732958
  }
732848
- if (h3.node.dirty && !ro) {
732959
+ if (h3.node.dirty && !ro && !h3.node.localOnly) {
732849
732960
  if (h3.node.isNew || !h3.node.id) await this.uploadNew(h3.node);
732850
732961
  else await this.uploadUpdate(h3.node);
732851
732962
  h3.node.dirty = false;
732852
732963
  h3.node.mtime = /* @__PURE__ */ new Date();
732964
+ } else if (h3.node.dirty && h3.node.localOnly) {
732965
+ h3.node.dirty = false;
732966
+ h3.node.mtime = /* @__PURE__ */ new Date();
732853
732967
  }
732854
732968
  cb(0);
732855
732969
  }),
@@ -732859,7 +732973,9 @@ var HubdocFs = class {
732859
732973
  if (ro) return cb(Fuse.EACCES);
732860
732974
  const node = await this.resolve(p3);
732861
732975
  if (!node || node.kind !== "file") return cb(Fuse.ENOENT);
732862
- if (node.id) await this.ctx.axios.delete(`/api/v1/documents/documents/${node.id}`);
732976
+ if (node.id && !node.localOnly) {
732977
+ await this.ctx.axios.delete(`/api/v1/documents/documents/${node.id}`);
732978
+ }
732863
732979
  if (node.cachePath) await import_fs_extra32.default.remove(node.cachePath).catch(() => {
732864
732980
  });
732865
732981
  this.nodes.delete(p3);
@@ -732907,6 +733023,56 @@ var HubdocFs = class {
732907
733023
  if (!newParent || newParent.kind !== "dir" || newParent.hub === "root") return cb(Fuse.EPERM);
732908
733024
  const newName = import_path34.default.basename(dest);
732909
733025
  const newFolderId = newParent.hub === "folder" ? newParent.folderId : null;
733026
+ if (node.localOnly) {
733027
+ this.nodes.delete(src);
733028
+ node.name = newName;
733029
+ node.folderId = node.kind === "file" ? newFolderId ?? void 0 : node.folderId;
733030
+ node.workspaceId = newParent.workspaceId;
733031
+ this.nodes.set(dest, node);
733032
+ this.invalidateParent(src);
733033
+ this.invalidateParent(dest);
733034
+ return cb(0);
733035
+ }
733036
+ let destNode = null;
733037
+ try {
733038
+ destNode = await this.resolve(dest);
733039
+ } catch {
733040
+ }
733041
+ if (node.kind === "file" && !node.id) {
733042
+ if (destNode && destNode.kind === "file" && destNode.id) {
733043
+ const sourceCache = this.cacheFileFor(node);
733044
+ const local = this.cacheFileFor(destNode);
733045
+ await import_fs_extra32.default.copy(sourceCache, local, { overwrite: true });
733046
+ destNode.dirty = true;
733047
+ destNode.downloaded = true;
733048
+ destNode.name = newName;
733049
+ destNode.folderId = newFolderId ?? void 0;
733050
+ destNode.workspaceId = newParent.workspaceId;
733051
+ await this.uploadNode(destNode, "update");
733052
+ destNode.dirty = false;
733053
+ destNode.mtime = /* @__PURE__ */ new Date();
733054
+ this.nodes.delete(src);
733055
+ await import_fs_extra32.default.remove(sourceCache).catch(() => {
733056
+ });
733057
+ this.invalidateParent(src);
733058
+ this.invalidateParent(dest);
733059
+ return cb(0);
733060
+ }
733061
+ node.name = newName;
733062
+ node.folderId = newFolderId ?? void 0;
733063
+ node.workspaceId = newParent.workspaceId;
733064
+ await this.uploadNew(node);
733065
+ this.nodes.delete(src);
733066
+ this.nodes.set(dest, node);
733067
+ this.invalidateParent(src);
733068
+ this.invalidateParent(dest);
733069
+ return cb(0);
733070
+ }
733071
+ if (destNode && destNode !== node && destNode.kind === "file" && destNode.id) {
733072
+ await this.ctx.axios.delete(`/api/v1/documents/documents/${destNode.id}`).catch(() => {
733073
+ });
733074
+ this.nodes.delete(dest);
733075
+ }
732910
733076
  const resource = node.kind === "file" ? "documents" : "folders";
732911
733077
  if (!node.id) return cb(Fuse.EPERM);
732912
733078
  await this.ctx.axios.patch(`/api/v1/documents/${resource}/${node.id}`, {
@@ -732940,7 +733106,7 @@ var HubdocFs = class {
732940
733106
  }
732941
733107
  this.fuse = Fuse;
732942
733108
  await import_fs_extra32.default.ensureDir(mountPath);
732943
- const fuse = new Fuse(mountPath, this.ops(), {
733109
+ const fuse = new Fuse(mountPath, this.getOps(), {
732944
733110
  force: true,
732945
733111
  mkdir: true,
732946
733112
  displayFolder: "Hubdoc",
@@ -733760,7 +733926,7 @@ async function handlePermissionsShow(id, opts) {
733760
733926
  // apps/cli/cli.ts
733761
733927
  var getVersion = () => {
733762
733928
  try {
733763
- if (true) return "1.8.1";
733929
+ if (true) return "1.8.2";
733764
733930
  } catch {
733765
733931
  }
733766
733932
  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.2",
4
4
  "description": "Professional command-line tool for HubDoc document management and bulk import/export",
5
5
  "main": "cli.js",
6
6
  "bin": {