@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 +18 -1
- package/dist/dav/dav.d.ts +11 -0
- package/dist/files/node.d.ts +11 -0
- package/dist/files/nodeData.d.ts +2 -0
- package/dist/index.cjs +138 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.mjs +139 -32
- package/dist/index.mjs.map +1 -1
- package/dist/utils/filename-validation.d.ts +51 -0
- package/dist/utils/filename.d.ts +0 -6
- package/dist/utils/logger.d.ts +0 -4
- package/package.json +12 -10
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,8 @@ export { FileType } from './files/fileType';
|
|
|
12
12
|
export { File, type IFile } from './files/file';
|
|
13
13
|
export { Folder, type IFolder } from './files/folder';
|
|
14
14
|
export { Node, NodeStatus, type INode } from './files/node';
|
|
15
|
-
export
|
|
15
|
+
export * from './utils/filename-validation';
|
|
16
|
+
export { getUniqueName } from './utils/filename';
|
|
16
17
|
export { formatFileSize, parseFileSize } from './utils/fileSize';
|
|
17
18
|
export { orderBy } from './utils/sorting';
|
|
18
19
|
export { sortNodes, FilesSortingMode, type FilesSortingOptions } from './utils/fileSorting';
|
package/dist/index.mjs
CHANGED
|
@@ -1,19 +1,15 @@
|
|
|
1
|
-
import { getCurrentUser, onRequestTokenUpdate, getRequestToken } from "@nextcloud/auth";
|
|
2
1
|
import { getLoggerBuilder } from "@nextcloud/logger";
|
|
2
|
+
import { getCurrentUser, onRequestTokenUpdate, getRequestToken } from "@nextcloud/auth";
|
|
3
3
|
import { join, basename, extname, dirname } from "path";
|
|
4
4
|
import { encodePath } from "@nextcloud/paths";
|
|
5
5
|
import { generateRemoteUrl } from "@nextcloud/router";
|
|
6
|
-
import { createClient, getPatcher } from "webdav";
|
|
7
6
|
import { CancelablePromise } from "cancelable-promise";
|
|
7
|
+
import { createClient, getPatcher } from "webdav";
|
|
8
|
+
import { isPublicShare, getSharingToken } from "@nextcloud/sharing/public";
|
|
9
|
+
import { getCapabilities } from "@nextcloud/capabilities";
|
|
8
10
|
import { getCanonicalLocale, getLanguage } from "@nextcloud/l10n";
|
|
9
11
|
import { TypedEventTarget } from "typescript-event-target";
|
|
10
|
-
const
|
|
11
|
-
if (user === null) {
|
|
12
|
-
return getLoggerBuilder().setApp("files").build();
|
|
13
|
-
}
|
|
14
|
-
return getLoggerBuilder().setApp("files").setUid(user.uid).build();
|
|
15
|
-
};
|
|
16
|
-
const logger = getLogger(getCurrentUser());
|
|
12
|
+
const logger = getLoggerBuilder().setApp("@nextcloud/files").detectUser().build();
|
|
17
13
|
var NewMenuEntryCategory = /* @__PURE__ */ ((NewMenuEntryCategory2) => {
|
|
18
14
|
NewMenuEntryCategory2[NewMenuEntryCategory2["UploadFromDevice"] = 0] = "UploadFromDevice";
|
|
19
15
|
NewMenuEntryCategory2[NewMenuEntryCategory2["CreateNew"] = 1] = "CreateNew";
|
|
@@ -256,6 +252,8 @@ const defaultDavProperties = [
|
|
|
256
252
|
"d:getcontenttype",
|
|
257
253
|
"d:getetag",
|
|
258
254
|
"d:getlastmodified",
|
|
255
|
+
"d:creationdate",
|
|
256
|
+
"d:displayname",
|
|
259
257
|
"d:quota-available-bytes",
|
|
260
258
|
"d:resourcetype",
|
|
261
259
|
"nc:has-preview",
|
|
@@ -408,6 +406,11 @@ const davParsePermissions = function(permString = "") {
|
|
|
408
406
|
}
|
|
409
407
|
return permissions;
|
|
410
408
|
};
|
|
409
|
+
var FileType = /* @__PURE__ */ ((FileType2) => {
|
|
410
|
+
FileType2["Folder"] = "folder";
|
|
411
|
+
FileType2["File"] = "file";
|
|
412
|
+
return FileType2;
|
|
413
|
+
})(FileType || {});
|
|
411
414
|
const isDavRessource = function(source, davService) {
|
|
412
415
|
return source.match(davService) !== null;
|
|
413
416
|
};
|
|
@@ -426,6 +429,9 @@ const validateData = (data, davService) => {
|
|
|
426
429
|
if (!data.source.startsWith("http")) {
|
|
427
430
|
throw new Error("Invalid source format, only http(s) is supported");
|
|
428
431
|
}
|
|
432
|
+
if (data.displayname && typeof data.displayname !== "string") {
|
|
433
|
+
throw new Error("Invalid displayname type");
|
|
434
|
+
}
|
|
429
435
|
if (data.mtime && !(data.mtime instanceof Date)) {
|
|
430
436
|
throw new Error("Invalid mtime type");
|
|
431
437
|
}
|
|
@@ -502,7 +508,12 @@ class Node {
|
|
|
502
508
|
};
|
|
503
509
|
constructor(data, davService) {
|
|
504
510
|
validateData(data, davService || this._knownDavService);
|
|
505
|
-
this._data = {
|
|
511
|
+
this._data = {
|
|
512
|
+
// TODO: Remove with next major release, this is just for compatibility
|
|
513
|
+
displayname: data.attributes?.displayname,
|
|
514
|
+
...data,
|
|
515
|
+
attributes: {}
|
|
516
|
+
};
|
|
506
517
|
this._attributes = new Proxy(this._data.attributes, this.handler);
|
|
507
518
|
this.update(data.attributes ?? {});
|
|
508
519
|
if (davService) {
|
|
@@ -532,6 +543,21 @@ class Node {
|
|
|
532
543
|
get basename() {
|
|
533
544
|
return basename(this.source);
|
|
534
545
|
}
|
|
546
|
+
/**
|
|
547
|
+
* The nodes displayname
|
|
548
|
+
* By default the display name and the `basename` are identical,
|
|
549
|
+
* but it is possible to have a different name. This happens
|
|
550
|
+
* on the files app for example for shared folders.
|
|
551
|
+
*/
|
|
552
|
+
get displayname() {
|
|
553
|
+
return this._data.displayname || this.basename;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Set the displayname
|
|
557
|
+
*/
|
|
558
|
+
set displayname(displayname) {
|
|
559
|
+
this._data.displayname = displayname;
|
|
560
|
+
}
|
|
535
561
|
/**
|
|
536
562
|
* Get this object's extension
|
|
537
563
|
* There is no setter as the source is not meant to be changed manually.
|
|
@@ -694,7 +720,11 @@ class Node {
|
|
|
694
720
|
*/
|
|
695
721
|
move(destination) {
|
|
696
722
|
validateData({ ...this._data, source: destination }, this._knownDavService);
|
|
723
|
+
const oldBasename = this.basename;
|
|
697
724
|
this._data.source = destination;
|
|
725
|
+
if (this.displayname === oldBasename && this.basename !== oldBasename) {
|
|
726
|
+
this.displayname = this.basename;
|
|
727
|
+
}
|
|
698
728
|
this.updateMtime();
|
|
699
729
|
}
|
|
700
730
|
/**
|
|
@@ -740,11 +770,6 @@ class Node {
|
|
|
740
770
|
}
|
|
741
771
|
}
|
|
742
772
|
}
|
|
743
|
-
var FileType = /* @__PURE__ */ ((FileType2) => {
|
|
744
|
-
FileType2["Folder"] = "folder";
|
|
745
|
-
FileType2["File"] = "file";
|
|
746
|
-
return FileType2;
|
|
747
|
-
})(FileType || {});
|
|
748
773
|
class File extends Node {
|
|
749
774
|
get type() {
|
|
750
775
|
return FileType.File;
|
|
@@ -767,8 +792,21 @@ class Folder extends Node {
|
|
|
767
792
|
return "httpd/unix-directory";
|
|
768
793
|
}
|
|
769
794
|
}
|
|
770
|
-
|
|
771
|
-
|
|
795
|
+
function davGetRootPath() {
|
|
796
|
+
if (isPublicShare()) {
|
|
797
|
+
return `/files/${getSharingToken()}`;
|
|
798
|
+
}
|
|
799
|
+
return `/files/${getCurrentUser()?.uid}`;
|
|
800
|
+
}
|
|
801
|
+
const davRootPath = davGetRootPath();
|
|
802
|
+
function davGetRemoteURL() {
|
|
803
|
+
const url = generateRemoteUrl("dav");
|
|
804
|
+
if (isPublicShare()) {
|
|
805
|
+
return url.replace("remote.php", "public.php");
|
|
806
|
+
}
|
|
807
|
+
return url;
|
|
808
|
+
}
|
|
809
|
+
const davRemoteURL = davGetRemoteURL();
|
|
772
810
|
const davGetClient = function(remoteURL = davRemoteURL, headers = {}) {
|
|
773
811
|
const client = createClient(remoteURL, { headers });
|
|
774
812
|
function setHeaders(token) {
|
|
@@ -817,9 +855,7 @@ const getFavoriteNodes = (davClient, path = "/", davRoot = davRootPath) => {
|
|
|
817
855
|
};
|
|
818
856
|
const davResultToNode = function(node, filesRoot = davRootPath, remoteURL = davRemoteURL) {
|
|
819
857
|
let userId = getCurrentUser()?.uid;
|
|
820
|
-
|
|
821
|
-
if (isPublic) {
|
|
822
|
-
userId = userId ?? document.querySelector("input#sharingUserId")?.value;
|
|
858
|
+
if (isPublicShare()) {
|
|
823
859
|
userId = userId ?? "anonymous";
|
|
824
860
|
} else if (!userId) {
|
|
825
861
|
throw new Error("No user id found");
|
|
@@ -833,6 +869,7 @@ const davResultToNode = function(node, filesRoot = davRootPath, remoteURL = davR
|
|
|
833
869
|
source: `${remoteURL}${node.filename}`,
|
|
834
870
|
mtime: new Date(Date.parse(node.lastmod)),
|
|
835
871
|
mime: node.mime || "application/octet-stream",
|
|
872
|
+
displayname: props.displayname,
|
|
836
873
|
size: props?.size || Number.parseInt(props.getcontentlength || "0"),
|
|
837
874
|
// The fileid is set to -1 for failed requests
|
|
838
875
|
status: id < 0 ? NodeStatus.FAILED : void 0,
|
|
@@ -848,16 +885,81 @@ const davResultToNode = function(node, filesRoot = davRootPath, remoteURL = davR
|
|
|
848
885
|
delete nodeData.attributes?.props;
|
|
849
886
|
return node.type === "file" ? new File(nodeData) : new Folder(nodeData);
|
|
850
887
|
};
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
888
|
+
var InvalidFilenameErrorReason = /* @__PURE__ */ ((InvalidFilenameErrorReason2) => {
|
|
889
|
+
InvalidFilenameErrorReason2["ReservedName"] = "reserved name";
|
|
890
|
+
InvalidFilenameErrorReason2["Character"] = "character";
|
|
891
|
+
InvalidFilenameErrorReason2["Extension"] = "extension";
|
|
892
|
+
return InvalidFilenameErrorReason2;
|
|
893
|
+
})(InvalidFilenameErrorReason || {});
|
|
894
|
+
class InvalidFilenameError extends Error {
|
|
895
|
+
constructor(options) {
|
|
896
|
+
super(`Invalid ${options.reason} '${options.segment}' in filename '${options.filename}'`, { cause: options });
|
|
856
897
|
}
|
|
857
|
-
|
|
858
|
-
|
|
898
|
+
/**
|
|
899
|
+
* The filename that was validated
|
|
900
|
+
*/
|
|
901
|
+
get filename() {
|
|
902
|
+
return this.cause.filename;
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Reason why the validation failed
|
|
906
|
+
*/
|
|
907
|
+
get reason() {
|
|
908
|
+
return this.cause.reason;
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Part of the filename that caused this error
|
|
912
|
+
*/
|
|
913
|
+
get segment() {
|
|
914
|
+
return this.cause.segment;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
function validateFilename(filename) {
|
|
918
|
+
const capabilities = getCapabilities().files;
|
|
919
|
+
const forbiddenCharacters = capabilities.forbidden_filename_characters ?? window._oc_config?.forbidden_filenames_characters ?? ["/", "\\"];
|
|
920
|
+
for (const character of forbiddenCharacters) {
|
|
921
|
+
if (filename.includes(character)) {
|
|
922
|
+
throw new InvalidFilenameError({ segment: character, reason: "character", filename });
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
filename = filename.toLocaleLowerCase();
|
|
926
|
+
const forbiddenFilenames = capabilities.forbidden_filenames ?? [".htaccess"];
|
|
927
|
+
if (forbiddenFilenames.includes(filename)) {
|
|
928
|
+
throw new InvalidFilenameError({
|
|
929
|
+
filename,
|
|
930
|
+
segment: filename,
|
|
931
|
+
reason: "reserved name"
|
|
932
|
+
/* ReservedName */
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
const endOfBasename = filename.indexOf(".", 1);
|
|
936
|
+
const basename2 = filename.substring(0, endOfBasename === -1 ? void 0 : endOfBasename);
|
|
937
|
+
const forbiddenFilenameBasenames = capabilities.forbidden_filename_basenames ?? [];
|
|
938
|
+
if (forbiddenFilenameBasenames.includes(basename2)) {
|
|
939
|
+
throw new InvalidFilenameError({
|
|
940
|
+
filename,
|
|
941
|
+
segment: basename2,
|
|
942
|
+
reason: "reserved name"
|
|
943
|
+
/* ReservedName */
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
const forbiddenFilenameExtensions = capabilities.forbidden_filename_extensions ?? [".part", ".filepart"];
|
|
947
|
+
for (const extension of forbiddenFilenameExtensions) {
|
|
948
|
+
if (filename.length > extension.length && filename.endsWith(extension)) {
|
|
949
|
+
throw new InvalidFilenameError({ segment: extension, reason: "extension", filename });
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
function isFilenameValid(filename) {
|
|
954
|
+
try {
|
|
955
|
+
validateFilename(filename);
|
|
956
|
+
return true;
|
|
957
|
+
} catch (error) {
|
|
958
|
+
if (error instanceof InvalidFilenameError) {
|
|
959
|
+
return false;
|
|
960
|
+
}
|
|
961
|
+
throw error;
|
|
859
962
|
}
|
|
860
|
-
return true;
|
|
861
963
|
}
|
|
862
964
|
function getUniqueName(name, otherNames, options) {
|
|
863
965
|
const opts = {
|
|
@@ -966,10 +1068,10 @@ function sortNodes(nodes, options = {}) {
|
|
|
966
1068
|
...sortingOptions.sortFavoritesFirst ? [(v) => v.attributes?.favorite !== 1] : [],
|
|
967
1069
|
// 2: Sort folders first if sorting by name
|
|
968
1070
|
...sortingOptions.sortFoldersFirst ? [(v) => v.type !== "folder"] : [],
|
|
969
|
-
// 3: Use sorting mode if NOT basename (to be able to use
|
|
1071
|
+
// 3: Use sorting mode if NOT basename (to be able to use displayname too)
|
|
970
1072
|
...sortingOptions.sortingMode !== "basename" ? [(v) => v[sortingOptions.sortingMode]] : [],
|
|
971
|
-
// 4: Use
|
|
972
|
-
(v) => basename2(v.attributes?.
|
|
1073
|
+
// 4: Use displayname if available, fallback to name
|
|
1074
|
+
(v) => basename2(v.attributes?.displayname || v.basename),
|
|
973
1075
|
// 5: Finally, use basename if all previous sorting methods failed
|
|
974
1076
|
(v) => v.basename
|
|
975
1077
|
];
|
|
@@ -2804,6 +2906,8 @@ export {
|
|
|
2804
2906
|
FilesSortingMode,
|
|
2805
2907
|
Folder,
|
|
2806
2908
|
Header,
|
|
2909
|
+
InvalidFilenameError,
|
|
2910
|
+
InvalidFilenameErrorReason,
|
|
2807
2911
|
Navigation,
|
|
2808
2912
|
NewMenuEntryCategory,
|
|
2809
2913
|
Node,
|
|
@@ -2815,6 +2919,8 @@ export {
|
|
|
2815
2919
|
davGetDefaultPropfind,
|
|
2816
2920
|
davGetFavoritesReport,
|
|
2817
2921
|
davGetRecentSearch,
|
|
2922
|
+
davGetRemoteURL,
|
|
2923
|
+
davGetRootPath,
|
|
2818
2924
|
davParsePermissions,
|
|
2819
2925
|
davRemoteURL,
|
|
2820
2926
|
davResultToNode,
|
|
@@ -2837,5 +2943,6 @@ export {
|
|
|
2837
2943
|
registerFileAction,
|
|
2838
2944
|
registerFileListHeaders,
|
|
2839
2945
|
removeNewFileMenuEntry,
|
|
2840
|
-
sortNodes
|
|
2946
|
+
sortNodes,
|
|
2947
|
+
validateFilename
|
|
2841
2948
|
};
|