@nextcloud/files 3.5.0 → 3.6.0

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/README.md CHANGED
@@ -5,7 +5,9 @@
5
5
  # @nextcloud/files
6
6
  [![npm last version](https://img.shields.io/npm/v/@nextcloud/files.svg?style=flat-square)](https://www.npmjs.com/package/@nextcloud/files) [![REUSE status](https://api.reuse.software/badge/github.com/nextcloud-libraries/nextcloud-files)](https://api.reuse.software/info/github.com/nextcloud-libraries/nextcloud-files) [![Code coverage](https://img.shields.io/codecov/c/github/nextcloud-libraries/nextcloud-files?style=flat-square)](https://app.codecov.io/gh/nextcloud-libraries/nextcloud-files) [![Project documentation](https://img.shields.io/badge/documentation-online-blue?style=flat-square)](https://nextcloud-libraries.github.io/nextcloud-files/)
7
7
 
8
- Nextcloud Files helpers for Nextcloud apps and libraries
8
+ Nextcloud Files helpers for Nextcloud apps and libraries.
9
+
10
+ The `davGetClient` exported function returns a webDAV client that's a wrapper around [webdav's webDAV client](https://www.npmjs.com/package/webdav); All its methods are available here.
9
11
 
10
12
  ## Usage example
11
13
 
@@ -57,3 +59,18 @@ const nodes = results.data.map((result) => davResultToNode(r, myRoot))
57
59
  const nodes = results.data.map((result) => davResultToNode(r, myRoot, myRemoteURL))
58
60
 
59
61
  ```
62
+
63
+ ### Using WebDAV to get a Node from a file's name
64
+
65
+ ```ts
66
+ import { davGetClient, davGetDefaultPropfind, davResultToNode, davRootPath } from '@nextcloud/files'
67
+ import { emit } from '@nextcloud/event-bus'
68
+ const client = davGetClient()
69
+ client.stat(`${davRootPath}${filename}`, {
70
+ details: true,
71
+ data: davGetDefaultPropfind(),
72
+ }).then((result) => {
73
+ const node = davResultToNode(result.data)
74
+ emit('files:node:updated', node)
75
+ })
76
+ ```
package/dist/dav/dav.d.ts CHANGED
@@ -2,12 +2,23 @@ import { FileStat, WebDAVClient } from 'webdav';
2
2
  import { Node } from '../files/node';
3
3
  import { CancelablePromise } from 'cancelable-promise';
4
4
 
5
+ /**
6
+ * Get the DAV root path for the current user or public share
7
+ */
8
+ export declare function davGetRootPath(): string;
5
9
  /**
6
10
  * The DAV root path for the current user
11
+ * This is a cached version of `davGetRemoteURL`
7
12
  */
8
13
  export declare const davRootPath: string;
14
+ /**
15
+ * Get the DAV remote URL used as base URL for the WebDAV client
16
+ * It also handles public shares
17
+ */
18
+ export declare function davGetRemoteURL(): string;
9
19
  /**
10
20
  * The DAV remote URL used as base URL for the WebDAV client
21
+ * This is a cached version of `davGetRemoteURL`
11
22
  */
12
23
  export declare const davRemoteURL: string;
13
24
  /**
@@ -35,6 +35,17 @@ export declare abstract class Node {
35
35
  * You can use the rename or move method to change the source.
36
36
  */
37
37
  get basename(): string;
38
+ /**
39
+ * The nodes displayname
40
+ * By default the display name and the `basename` are identical,
41
+ * but it is possible to have a different name. This happens
42
+ * on the files app for example for shared folders.
43
+ */
44
+ get displayname(): string;
45
+ /**
46
+ * Set the displayname
47
+ */
48
+ set displayname(displayname: string);
38
49
  /**
39
50
  * Get this object's extension
40
51
  * There is no setter as the source is not meant to be changed manually.
@@ -25,6 +25,8 @@ export interface NodeData {
25
25
  permissions?: Permission;
26
26
  /** The owner UID of this node */
27
27
  owner: string | null;
28
+ /** Optional the displayname of this node */
29
+ displayname?: string;
28
30
  /** The node attributes */
29
31
  attributes?: Attribute;
30
32
  /**
package/dist/index.cjs CHANGED
@@ -1,21 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const auth = require("@nextcloud/auth");
4
3
  const logger$1 = require("@nextcloud/logger");
4
+ const auth = require("@nextcloud/auth");
5
5
  const path = require("path");
6
6
  const paths = require("@nextcloud/paths");
7
7
  const router = require("@nextcloud/router");
8
- const webdav = require("webdav");
9
8
  const cancelablePromise = require("cancelable-promise");
9
+ const webdav = require("webdav");
10
+ const _public = require("@nextcloud/sharing/public");
11
+ const capabilities = require("@nextcloud/capabilities");
10
12
  const l10n = require("@nextcloud/l10n");
11
13
  const typescriptEventTarget = require("typescript-event-target");
12
- const getLogger = (user) => {
13
- if (user === null) {
14
- return logger$1.getLoggerBuilder().setApp("files").build();
15
- }
16
- return logger$1.getLoggerBuilder().setApp("files").setUid(user.uid).build();
17
- };
18
- const logger = getLogger(auth.getCurrentUser());
14
+ const logger = logger$1.getLoggerBuilder().setApp("@nextcloud/files").detectUser().build();
19
15
  var NewMenuEntryCategory = /* @__PURE__ */ ((NewMenuEntryCategory2) => {
20
16
  NewMenuEntryCategory2[NewMenuEntryCategory2["UploadFromDevice"] = 0] = "UploadFromDevice";
21
17
  NewMenuEntryCategory2[NewMenuEntryCategory2["CreateNew"] = 1] = "CreateNew";
@@ -258,6 +254,8 @@ const defaultDavProperties = [
258
254
  "d:getcontenttype",
259
255
  "d:getetag",
260
256
  "d:getlastmodified",
257
+ "d:creationdate",
258
+ "d:displayname",
261
259
  "d:quota-available-bytes",
262
260
  "d:resourcetype",
263
261
  "nc:has-preview",
@@ -410,6 +408,11 @@ const davParsePermissions = function(permString = "") {
410
408
  }
411
409
  return permissions;
412
410
  };
411
+ var FileType = /* @__PURE__ */ ((FileType2) => {
412
+ FileType2["Folder"] = "folder";
413
+ FileType2["File"] = "file";
414
+ return FileType2;
415
+ })(FileType || {});
413
416
  const isDavRessource = function(source, davService) {
414
417
  return source.match(davService) !== null;
415
418
  };
@@ -428,6 +431,9 @@ const validateData = (data, davService) => {
428
431
  if (!data.source.startsWith("http")) {
429
432
  throw new Error("Invalid source format, only http(s) is supported");
430
433
  }
434
+ if (data.displayname && typeof data.displayname !== "string") {
435
+ throw new Error("Invalid displayname type");
436
+ }
431
437
  if (data.mtime && !(data.mtime instanceof Date)) {
432
438
  throw new Error("Invalid mtime type");
433
439
  }
@@ -504,7 +510,12 @@ class Node {
504
510
  };
505
511
  constructor(data, davService) {
506
512
  validateData(data, davService || this._knownDavService);
507
- this._data = { ...data, attributes: {} };
513
+ this._data = {
514
+ // TODO: Remove with next major release, this is just for compatibility
515
+ displayname: data.attributes?.displayname,
516
+ ...data,
517
+ attributes: {}
518
+ };
508
519
  this._attributes = new Proxy(this._data.attributes, this.handler);
509
520
  this.update(data.attributes ?? {});
510
521
  if (davService) {
@@ -534,6 +545,21 @@ class Node {
534
545
  get basename() {
535
546
  return path.basename(this.source);
536
547
  }
548
+ /**
549
+ * The nodes displayname
550
+ * By default the display name and the `basename` are identical,
551
+ * but it is possible to have a different name. This happens
552
+ * on the files app for example for shared folders.
553
+ */
554
+ get displayname() {
555
+ return this._data.displayname || this.basename;
556
+ }
557
+ /**
558
+ * Set the displayname
559
+ */
560
+ set displayname(displayname) {
561
+ this._data.displayname = displayname;
562
+ }
537
563
  /**
538
564
  * Get this object's extension
539
565
  * There is no setter as the source is not meant to be changed manually.
@@ -696,7 +722,11 @@ class Node {
696
722
  */
697
723
  move(destination) {
698
724
  validateData({ ...this._data, source: destination }, this._knownDavService);
725
+ const oldBasename = this.basename;
699
726
  this._data.source = destination;
727
+ if (this.displayname === oldBasename && this.basename !== oldBasename) {
728
+ this.displayname = this.basename;
729
+ }
700
730
  this.updateMtime();
701
731
  }
702
732
  /**
@@ -742,11 +772,6 @@ class Node {
742
772
  }
743
773
  }
744
774
  }
745
- var FileType = /* @__PURE__ */ ((FileType2) => {
746
- FileType2["Folder"] = "folder";
747
- FileType2["File"] = "file";
748
- return FileType2;
749
- })(FileType || {});
750
775
  class File extends Node {
751
776
  get type() {
752
777
  return FileType.File;
@@ -769,8 +794,21 @@ class Folder extends Node {
769
794
  return "httpd/unix-directory";
770
795
  }
771
796
  }
772
- const davRootPath = `/files/${auth.getCurrentUser()?.uid}`;
773
- const davRemoteURL = router.generateRemoteUrl("dav");
797
+ function davGetRootPath() {
798
+ if (_public.isPublicShare()) {
799
+ return `/files/${_public.getSharingToken()}`;
800
+ }
801
+ return `/files/${auth.getCurrentUser()?.uid}`;
802
+ }
803
+ const davRootPath = davGetRootPath();
804
+ function davGetRemoteURL() {
805
+ const url = router.generateRemoteUrl("dav");
806
+ if (_public.isPublicShare()) {
807
+ return url.replace("remote.php", "public.php");
808
+ }
809
+ return url;
810
+ }
811
+ const davRemoteURL = davGetRemoteURL();
774
812
  const davGetClient = function(remoteURL = davRemoteURL, headers = {}) {
775
813
  const client = webdav.createClient(remoteURL, { headers });
776
814
  function setHeaders(token) {
@@ -819,9 +857,7 @@ const getFavoriteNodes = (davClient, path2 = "/", davRoot = davRootPath) => {
819
857
  };
820
858
  const davResultToNode = function(node, filesRoot = davRootPath, remoteURL = davRemoteURL) {
821
859
  let userId = auth.getCurrentUser()?.uid;
822
- const isPublic = document.querySelector("input#isPublic")?.value;
823
- if (isPublic) {
824
- userId = userId ?? document.querySelector("input#sharingUserId")?.value;
860
+ if (_public.isPublicShare()) {
825
861
  userId = userId ?? "anonymous";
826
862
  } else if (!userId) {
827
863
  throw new Error("No user id found");
@@ -835,6 +871,7 @@ const davResultToNode = function(node, filesRoot = davRootPath, remoteURL = davR
835
871
  source: `${remoteURL}${node.filename}`,
836
872
  mtime: new Date(Date.parse(node.lastmod)),
837
873
  mime: node.mime || "application/octet-stream",
874
+ displayname: props.displayname,
838
875
  size: props?.size || Number.parseInt(props.getcontentlength || "0"),
839
876
  // The fileid is set to -1 for failed requests
840
877
  status: id < 0 ? NodeStatus.FAILED : void 0,
@@ -850,16 +887,81 @@ const davResultToNode = function(node, filesRoot = davRootPath, remoteURL = davR
850
887
  delete nodeData.attributes?.props;
851
888
  return node.type === "file" ? new File(nodeData) : new Folder(nodeData);
852
889
  };
853
- const forbiddenCharacters = window._oc_config?.forbidden_filenames_characters ?? ["/", "\\"];
854
- const forbiddenFilenameRegex = window._oc_config?.blacklist_files_regex ? new RegExp(window._oc_config.blacklist_files_regex) : null;
855
- function isFilenameValid(filename) {
856
- if (forbiddenCharacters.some((character) => filename.includes(character))) {
857
- return false;
890
+ var InvalidFilenameErrorReason = /* @__PURE__ */ ((InvalidFilenameErrorReason2) => {
891
+ InvalidFilenameErrorReason2["ReservedName"] = "reserved name";
892
+ InvalidFilenameErrorReason2["Character"] = "character";
893
+ InvalidFilenameErrorReason2["Extension"] = "extension";
894
+ return InvalidFilenameErrorReason2;
895
+ })(InvalidFilenameErrorReason || {});
896
+ class InvalidFilenameError extends Error {
897
+ constructor(options) {
898
+ super(`Invalid ${options.reason} '${options.segment}' in filename '${options.filename}'`, { cause: options });
858
899
  }
859
- if (forbiddenFilenameRegex !== null && filename.match(forbiddenFilenameRegex)) {
860
- return false;
900
+ /**
901
+ * The filename that was validated
902
+ */
903
+ get filename() {
904
+ return this.cause.filename;
905
+ }
906
+ /**
907
+ * Reason why the validation failed
908
+ */
909
+ get reason() {
910
+ return this.cause.reason;
911
+ }
912
+ /**
913
+ * Part of the filename that caused this error
914
+ */
915
+ get segment() {
916
+ return this.cause.segment;
917
+ }
918
+ }
919
+ function validateFilename(filename) {
920
+ const capabilities$1 = capabilities.getCapabilities().files;
921
+ const forbiddenCharacters = capabilities$1.forbidden_filename_characters ?? window._oc_config?.forbidden_filenames_characters ?? ["/", "\\"];
922
+ for (const character of forbiddenCharacters) {
923
+ if (filename.includes(character)) {
924
+ throw new InvalidFilenameError({ segment: character, reason: "character", filename });
925
+ }
926
+ }
927
+ filename = filename.toLocaleLowerCase();
928
+ const forbiddenFilenames = capabilities$1.forbidden_filenames ?? [".htaccess"];
929
+ if (forbiddenFilenames.includes(filename)) {
930
+ throw new InvalidFilenameError({
931
+ filename,
932
+ segment: filename,
933
+ reason: "reserved name"
934
+ /* ReservedName */
935
+ });
936
+ }
937
+ const endOfBasename = filename.indexOf(".", 1);
938
+ const basename = filename.substring(0, endOfBasename === -1 ? void 0 : endOfBasename);
939
+ const forbiddenFilenameBasenames = capabilities$1.forbidden_filename_basenames ?? [];
940
+ if (forbiddenFilenameBasenames.includes(basename)) {
941
+ throw new InvalidFilenameError({
942
+ filename,
943
+ segment: basename,
944
+ reason: "reserved name"
945
+ /* ReservedName */
946
+ });
947
+ }
948
+ const forbiddenFilenameExtensions = capabilities$1.forbidden_filename_extensions ?? [".part", ".filepart"];
949
+ for (const extension of forbiddenFilenameExtensions) {
950
+ if (filename.length > extension.length && filename.endsWith(extension)) {
951
+ throw new InvalidFilenameError({ segment: extension, reason: "extension", filename });
952
+ }
953
+ }
954
+ }
955
+ function isFilenameValid(filename) {
956
+ try {
957
+ validateFilename(filename);
958
+ return true;
959
+ } catch (error) {
960
+ if (error instanceof InvalidFilenameError) {
961
+ return false;
962
+ }
963
+ throw error;
861
964
  }
862
- return true;
863
965
  }
864
966
  function getUniqueName(name, otherNames, options) {
865
967
  const opts = {
@@ -968,10 +1070,10 @@ function sortNodes(nodes, options = {}) {
968
1070
  ...sortingOptions.sortFavoritesFirst ? [(v) => v.attributes?.favorite !== 1] : [],
969
1071
  // 2: Sort folders first if sorting by name
970
1072
  ...sortingOptions.sortFoldersFirst ? [(v) => v.type !== "folder"] : [],
971
- // 3: Use sorting mode if NOT basename (to be able to use displayName too)
1073
+ // 3: Use sorting mode if NOT basename (to be able to use displayname too)
972
1074
  ...sortingOptions.sortingMode !== "basename" ? [(v) => v[sortingOptions.sortingMode]] : [],
973
- // 4: Use displayName if available, fallback to name
974
- (v) => basename(v.attributes?.displayName || v.basename),
1075
+ // 4: Use displayname if available, fallback to name
1076
+ (v) => basename(v.attributes?.displayname || v.basename),
975
1077
  // 5: Finally, use basename if all previous sorting methods failed
976
1078
  (v) => v.basename
977
1079
  ];
@@ -2805,6 +2907,8 @@ exports.FileType = FileType;
2805
2907
  exports.FilesSortingMode = FilesSortingMode;
2806
2908
  exports.Folder = Folder;
2807
2909
  exports.Header = Header;
2910
+ exports.InvalidFilenameError = InvalidFilenameError;
2911
+ exports.InvalidFilenameErrorReason = InvalidFilenameErrorReason;
2808
2912
  exports.Navigation = Navigation;
2809
2913
  exports.NewMenuEntryCategory = NewMenuEntryCategory;
2810
2914
  exports.Node = Node;
@@ -2816,6 +2920,8 @@ exports.davGetClient = davGetClient;
2816
2920
  exports.davGetDefaultPropfind = davGetDefaultPropfind;
2817
2921
  exports.davGetFavoritesReport = davGetFavoritesReport;
2818
2922
  exports.davGetRecentSearch = davGetRecentSearch;
2923
+ exports.davGetRemoteURL = davGetRemoteURL;
2924
+ exports.davGetRootPath = davGetRootPath;
2819
2925
  exports.davParsePermissions = davParsePermissions;
2820
2926
  exports.davRemoteURL = davRemoteURL;
2821
2927
  exports.davResultToNode = davResultToNode;
@@ -2839,3 +2945,4 @@ exports.registerFileAction = registerFileAction;
2839
2945
  exports.registerFileListHeaders = registerFileListHeaders;
2840
2946
  exports.removeNewFileMenuEntry = removeNewFileMenuEntry;
2841
2947
  exports.sortNodes = sortNodes;
2948
+ exports.validateFilename = validateFilename;