@nextcloud/files 3.5.1 → 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
+ ```
@@ -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
@@ -8,6 +8,7 @@ const router = require("@nextcloud/router");
8
8
  const cancelablePromise = require("cancelable-promise");
9
9
  const webdav = require("webdav");
10
10
  const _public = require("@nextcloud/sharing/public");
11
+ const capabilities = require("@nextcloud/capabilities");
11
12
  const l10n = require("@nextcloud/l10n");
12
13
  const typescriptEventTarget = require("typescript-event-target");
13
14
  const logger = logger$1.getLoggerBuilder().setApp("@nextcloud/files").detectUser().build();
@@ -430,6 +431,9 @@ const validateData = (data, davService) => {
430
431
  if (!data.source.startsWith("http")) {
431
432
  throw new Error("Invalid source format, only http(s) is supported");
432
433
  }
434
+ if (data.displayname && typeof data.displayname !== "string") {
435
+ throw new Error("Invalid displayname type");
436
+ }
433
437
  if (data.mtime && !(data.mtime instanceof Date)) {
434
438
  throw new Error("Invalid mtime type");
435
439
  }
@@ -506,7 +510,12 @@ class Node {
506
510
  };
507
511
  constructor(data, davService) {
508
512
  validateData(data, davService || this._knownDavService);
509
- 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
+ };
510
519
  this._attributes = new Proxy(this._data.attributes, this.handler);
511
520
  this.update(data.attributes ?? {});
512
521
  if (davService) {
@@ -536,6 +545,21 @@ class Node {
536
545
  get basename() {
537
546
  return path.basename(this.source);
538
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
+ }
539
563
  /**
540
564
  * Get this object's extension
541
565
  * There is no setter as the source is not meant to be changed manually.
@@ -698,7 +722,11 @@ class Node {
698
722
  */
699
723
  move(destination) {
700
724
  validateData({ ...this._data, source: destination }, this._knownDavService);
725
+ const oldBasename = this.basename;
701
726
  this._data.source = destination;
727
+ if (this.displayname === oldBasename && this.basename !== oldBasename) {
728
+ this.displayname = this.basename;
729
+ }
702
730
  this.updateMtime();
703
731
  }
704
732
  /**
@@ -843,6 +871,7 @@ const davResultToNode = function(node, filesRoot = davRootPath, remoteURL = davR
843
871
  source: `${remoteURL}${node.filename}`,
844
872
  mtime: new Date(Date.parse(node.lastmod)),
845
873
  mime: node.mime || "application/octet-stream",
874
+ displayname: props.displayname,
846
875
  size: props?.size || Number.parseInt(props.getcontentlength || "0"),
847
876
  // The fileid is set to -1 for failed requests
848
877
  status: id < 0 ? NodeStatus.FAILED : void 0,
@@ -858,16 +887,81 @@ const davResultToNode = function(node, filesRoot = davRootPath, remoteURL = davR
858
887
  delete nodeData.attributes?.props;
859
888
  return node.type === "file" ? new File(nodeData) : new Folder(nodeData);
860
889
  };
861
- const forbiddenCharacters = window._oc_config?.forbidden_filenames_characters ?? ["/", "\\"];
862
- const forbiddenFilenameRegex = window._oc_config?.blacklist_files_regex ? new RegExp(window._oc_config.blacklist_files_regex) : null;
863
- function isFilenameValid(filename) {
864
- if (forbiddenCharacters.some((character) => filename.includes(character))) {
865
- 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 });
866
899
  }
867
- if (forbiddenFilenameRegex !== null && filename.match(forbiddenFilenameRegex)) {
868
- 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;
869
964
  }
870
- return true;
871
965
  }
872
966
  function getUniqueName(name, otherNames, options) {
873
967
  const opts = {
@@ -2813,6 +2907,8 @@ exports.FileType = FileType;
2813
2907
  exports.FilesSortingMode = FilesSortingMode;
2814
2908
  exports.Folder = Folder;
2815
2909
  exports.Header = Header;
2910
+ exports.InvalidFilenameError = InvalidFilenameError;
2911
+ exports.InvalidFilenameErrorReason = InvalidFilenameErrorReason;
2816
2912
  exports.Navigation = Navigation;
2817
2913
  exports.NewMenuEntryCategory = NewMenuEntryCategory;
2818
2914
  exports.Node = Node;
@@ -2849,3 +2945,4 @@ exports.registerFileAction = registerFileAction;
2849
2945
  exports.registerFileListHeaders = registerFileListHeaders;
2850
2946
  exports.removeNewFileMenuEntry = removeNewFileMenuEntry;
2851
2947
  exports.sortNodes = sortNodes;
2948
+ exports.validateFilename = validateFilename;