@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/README.md
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
# @nextcloud/files
|
|
6
6
|
[](https://www.npmjs.com/package/@nextcloud/files) [](https://api.reuse.software/info/github.com/nextcloud-libraries/nextcloud-files) [](https://app.codecov.io/gh/nextcloud-libraries/nextcloud-files) [](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
|
/**
|
package/dist/files/node.d.ts
CHANGED
|
@@ -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.
|
package/dist/files/nodeData.d.ts
CHANGED
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
|
|
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 = {
|
|
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
|
-
|
|
773
|
-
|
|
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
|
-
|
|
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
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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
|
-
|
|
860
|
-
|
|
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
|
|
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
|
|
974
|
-
(v) => basename(v.attributes?.
|
|
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;
|