UNPKG

54.9 kBJavaScriptView Raw
1"use strict";
2var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3 function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4 return new (P || (P = Promise))(function (resolve, reject) {
5 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7 function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8 step((generator = generator.apply(thisArg, _arguments || [])).next());
9 });
10};
11var __importDefault = (this && this.__importDefault) || function (mod) {
12 return (mod && mod.__esModule) ? mod : { "default": mod };
13};
14var __importStar = (this && this.__importStar) || function (mod) {
15 if (mod && mod.__esModule) return mod;
16 var result = {};
17 if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
18 result["default"] = mod;
19 return result;
20};
21Object.defineProperty(exports, "__esModule", { value: true });
22// tslint:disable-next-line:no-var-requires
23require("dotenv").config();
24const debug_1 = __importDefault(require("debug"));
25const fast_xml_parser_1 = __importDefault(require("fast-xml-parser"));
26const node_fetch_1 = require("node-fetch");
27const path_1 = __importStar(require("path"));
28const environment_1 = __importDefault(require("./environment"));
29const environmentVcapServices_1 = __importDefault(require("./environmentVcapServices"));
30const error_1 = __importDefault(require("./error"));
31exports.ClientError = error_1.default;
32const fakeServer_1 = __importDefault(require("./fakeServer"));
33exports.FakeServer = fakeServer_1.default;
34const file_1 = __importDefault(require("./file"));
35exports.File = file_1.default;
36const fileSystemElement_1 = __importDefault(require("./fileSystemElement"));
37exports.FileSystemElement = fileSystemElement_1.default;
38const folder_1 = __importDefault(require("./folder"));
39exports.Folder = folder_1.default;
40const httpClient_1 = require("./httpClient");
41const requestResponseLogEntry_1 = __importDefault(require("./requestResponseLogEntry"));
42exports.RequestResponseLogEntry = requestResponseLogEntry_1.default;
43const server_1 = __importDefault(require("./server"));
44exports.Server = server_1.default;
45const share_1 = __importStar(require("./share"));
46exports.SharePermission = share_1.SharePermission;
47const tag_1 = __importDefault(require("./tag"));
48exports.Tag = tag_1.default;
49const debug = debug_1.default("NCClient");
50/**
51 * The nextcloud client is the root object to access the remote api of the nextcloud server.<br>
52 */
53class Client {
54 /**
55 * Creates a new instance of a nextcloud client.<br/>
56 * Use the server to provide server connectivity information to the client.<br/>
57 * (The FakeServer is only used for testing and code coverage)<br/><br/>
58 * If the server is not provided the client tries to find the connectivity information
59 * in the environment.<br/>
60 * If a <b>VCAP_SERVICES</b> environment variable is available, the client tries to find
61 * a service with the name <b>"nextcloud"</b> in the user-provides-services section.<br/>
62 * If no VCAP_SERVICES are available, the client uses the following variables
63 * from the envirnonment for the connectivity:<br/>
64 * <ul>
65 * <li>NEXTCLOUD_URL - the WebDAV url of the nextcloud server</li>
66 * <li>NEXTCLOUD_USERNAME - the user name</li>
67 * <li>NEXTCLOUD_PASSWORD - the application password</li>
68 * </ul>
69 * @param server optional server information to connection to a nextcloud server
70 * @constructor
71 */
72 constructor(server) {
73 this.logRequestResponse = false;
74 debug("constructor");
75 this.nextcloudOrigin = "";
76 this.nextcloudAuthHeader = "";
77 this.nextcloudRequestToken = "";
78 this.webDAVUrl = "";
79 // if no server is provided, try to get a server from VCAP_S environment "nextcloud" instance
80 // If no VCAP_S environment exists try from environment
81 if (!server) {
82 try {
83 const env = new environmentVcapServices_1.default("nextcloud");
84 server = env.getServer();
85 }
86 catch (e) {
87 const env = new environment_1.default();
88 server = env.getServer();
89 }
90 }
91 if (server instanceof server_1.default) {
92 this.proxy = server.proxy;
93 debug("constructor: webdav url %s", server.url);
94 if (server.url.indexOf(Client.webDavUrlPath) === -1) {
95 // not a valid nextcloud url
96 throw new error_1.default(`The provided nextcloud url "${server.url}" does not comply to the nextcloud url standard, "${Client.webDavUrlPath}" is missing`, "ERR_INVALID_NEXTCLOUD_WEBDAV_URL");
97 }
98 this.nextcloudOrigin = server.url.substr(0, server.url.indexOf(Client.webDavUrlPath));
99 debug("constructor: nextcloud url %s", this.nextcloudOrigin);
100 this.nextcloudAuthHeader = "Basic " + Buffer.from(server.basicAuth.username + ":" + server.basicAuth.password).toString("base64");
101 this.nextcloudRequestToken = "";
102 if (server.url.slice(-1) === "/") {
103 this.webDAVUrl = server.url.slice(0, -1);
104 }
105 else {
106 this.webDAVUrl = server.url;
107 }
108 this.logRequestResponse = server.logRequestResponse;
109 const options = {
110 authorizationHeader: this.nextcloudAuthHeader,
111 logRequestResponse: this.logRequestResponse,
112 origin: this.nextcloudOrigin,
113 proxy: this.proxy,
114 };
115 this.httpClient = new httpClient_1.HttpClient(options);
116 }
117 if (server instanceof fakeServer_1.default) {
118 this.fakeServer = server;
119 this.webDAVUrl = "https://fake.server" + Client.webDavUrlPath;
120 }
121 }
122 /**
123 * returns the used and free quota of the nextcloud account
124 */
125 getQuota() {
126 return __awaiter(this, void 0, void 0, function* () {
127 debug("getQuota");
128 const requestInit = {
129 method: "PROPFIND",
130 };
131 const response = yield this.getHttpResponse(this.webDAVUrl + "/", requestInit, [207], { description: "Client get quota" });
132 const properties = yield this.getPropertiesFromWebDAVMultistatusResponse(response, Client.webDavUrlPath + "/");
133 let quota = null;
134 for (const prop of properties) {
135 if (prop["quota-available-bytes"]) {
136 quota = {
137 available: "unlimited",
138 used: prop["quota-used-bytes"],
139 };
140 if (prop["quota-available-bytes"] > 0) {
141 quota.available = prop["quota-available-bytes"];
142 }
143 }
144 }
145 if (!quota) {
146 debug("Error, quota not available: %s ", JSON.stringify(properties, null, 4));
147 throw new error_1.default(`Error, quota not available`, "ERR_QUOTA_NOT_AVAILABLE");
148 }
149 debug("getQuota = %O", quota);
150 return quota;
151 });
152 }
153 // ***************************************************************************************
154 // tags
155 // ***************************************************************************************
156 /**
157 * creates a new tag, if not already existing
158 * this function will fail with http 403 if the user does not have admin privileges
159 * @param tagName the name of the tag
160 * @returns tagId
161 */
162 createTag(tagName) {
163 return __awaiter(this, void 0, void 0, function* () {
164 debug("createTag");
165 let tag;
166 // is the tag already existing?
167 tag = yield this.getTagByName(tagName);
168 if (tag) {
169 return tag;
170 }
171 // tag does not exist, create tag
172 const requestInit = {
173 body: `{ "name": "${tagName}", "userVisible": true, "userAssignable": true, "canAssign": true }`,
174 headers: new node_fetch_1.Headers({ "Content-Type": "application/json" }),
175 method: "POST",
176 };
177 const response = yield this.getHttpResponse(this.nextcloudOrigin + "/remote.php/dav/systemtags/", requestInit, [201], { description: "Tag create" });
178 const tagString = response.headers.get("Content-Location");
179 debug("createTag new tagId %s, tagName %s", tagString, tagName);
180 if (tagString === "" || tagString === null) {
181 throw new error_1.default(`Error, tag with name '${tagName}' could not be created`, "ERR_TAG_CREATE_FAILED");
182 }
183 // the number id of the tag is the last element in the id (path)
184 const tagId = this.getTagIdFromHref(tagString);
185 tag = new tag_1.default(this, tagId, tagName, true, true, true);
186 return tag;
187 });
188 }
189 /**
190 * returns a tag identified by the name or null if not found
191 * @param tagName the name of the tag
192 * @returns tag or null
193 */
194 getTagByName(tagName) {
195 return __awaiter(this, void 0, void 0, function* () {
196 debug("getTag");
197 const tags = yield this.getTags();
198 for (const tag of tags) {
199 if (tag.name === tagName) {
200 return tag;
201 }
202 }
203 return null;
204 });
205 }
206 /**
207 * returns a tag identified by the id or null if not found
208 * @param tagId the id of the tag
209 * @returns tag or null
210 */
211 getTagById(tagId) {
212 return __awaiter(this, void 0, void 0, function* () {
213 debug("getTagById");
214 const tags = yield this.getTags();
215 for (const tag of tags) {
216 if (tag.id === tagId) {
217 return tag;
218 }
219 }
220 return null;
221 });
222 }
223 /**
224 * deletes the tag by id
225 * this function will fail with http 403 if the user does not have admin privileges
226 * @param tagId the id of the tag like "/remote.php/dav/systemtags/234"
227 */
228 deleteTag(tagId) {
229 return __awaiter(this, void 0, void 0, function* () {
230 debug("deleteTag tagId: $s", tagId);
231 const requestInit = {
232 method: "DELETE",
233 };
234 const response = yield this.getHttpResponse(`${this.nextcloudOrigin}/remote.php/dav/systemtags/${tagId}`, requestInit, [204, 404], { description: "Tag delete" });
235 });
236 }
237 /**
238 * deletes all visible assignable tags
239 * @throws Error
240 */
241 deleteAllTags() {
242 return __awaiter(this, void 0, void 0, function* () {
243 debug("deleteAllTags");
244 const tags = yield this.getTags();
245 for (const tag of tags) {
246 // debug("deleteAllTags tag: %O", tag);
247 yield tag.delete();
248 }
249 });
250 }
251 /**
252 * returns a list of tags
253 * @returns array of tags
254 */
255 getTags() {
256 return __awaiter(this, void 0, void 0, function* () {
257 debug("getTags PROPFIND %s", this.nextcloudOrigin + "/remote.php/dav/systemtags/");
258 const requestInit = {
259 body: `<?xml version="1.0"?>
260 <d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
261 <d:prop>
262 <oc:id />
263 <oc:display-name />
264 <oc:user-visible />
265 <oc:user-assignable />
266 <oc:can-assign />
267 </d:prop>
268 </d:propfind>`,
269 method: "PROPFIND",
270 };
271 const relUrl = `/remote.php/dav/systemtags/`;
272 const response = yield this.getHttpResponse(this.nextcloudOrigin + relUrl, requestInit, [207], { description: "Tags get" });
273 const properties = yield this.getPropertiesFromWebDAVMultistatusResponse(response, relUrl + "/*");
274 const tags = [];
275 for (const prop of properties) {
276 tags.push(new tag_1.default(this, this.getTagIdFromHref(prop._href), prop["display-name"], prop["user-visible"], prop["user-assignable"], prop["can-assign"]));
277 }
278 return tags;
279 });
280 }
281 /**
282 * returns the list of tag names and the tag ids
283 * @param fileId the id of the file
284 */
285 getTagsOfFile(fileId) {
286 return __awaiter(this, void 0, void 0, function* () {
287 debug("getTagsOfFile");
288 const requestInit = {
289 body: `<?xml version="1.0"?>
290 <d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
291 <d:prop>
292 <oc:id />
293 <oc:display-name />
294 <oc:user-visible />
295 <oc:user-assignable />
296 <oc:can-assign />
297 </d:prop>
298 </d:propfind>`,
299 method: "PROPFIND",
300 };
301 const relUrl = `/remote.php/dav/systemtags-relations/files/${fileId}`;
302 const response = yield this.getHttpResponse(`${this.nextcloudOrigin}${relUrl}`, requestInit, [207], { description: "File get tags" });
303 const properties = yield this.getPropertiesFromWebDAVMultistatusResponse(response, relUrl + "/*");
304 const tagMap = new Map();
305 for (const prop of properties) {
306 tagMap.set(prop["display-name"], prop.id);
307 }
308 debug("tags of file %O", tagMap);
309 return tagMap;
310 });
311 }
312 /**
313 * removes the tag from the file
314 * @param fileId the file id
315 * @param tagId the tag id
316 */
317 removeTagOfFile(fileId, tagId) {
318 return __awaiter(this, void 0, void 0, function* () {
319 debug("removeTagOfFile tagId: $s fileId:", tagId, fileId);
320 const requestInit = {
321 method: "DELETE",
322 };
323 const response = yield this.getHttpResponse(`${this.nextcloudOrigin}/remote.php/dav/systemtags-relations/files/${fileId}/${tagId}`, requestInit, [204, 404], { description: "File remove tag" });
324 return;
325 });
326 }
327 /**
328 * returns the id of the file or -1 of not found
329 * @returns id of the file or -1 if not found
330 */
331 getFileId(fileUrl) {
332 return __awaiter(this, void 0, void 0, function* () {
333 debug("getFileId");
334 const requestInit = {
335 body: `
336 <d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
337 <d:prop>
338 <oc:fileid />
339 </d:prop>
340 </d:propfind>`,
341 method: "PROPFIND",
342 };
343 const response = yield this.getHttpResponse(fileUrl, requestInit, [207], { description: "File get id" });
344 const properties = yield this.getPropertiesFromWebDAVMultistatusResponse(response, "");
345 for (const prop of properties) {
346 if (prop.fileid) {
347 return prop.fileid;
348 }
349 }
350 debug("getFileId no file id found for %s", fileUrl);
351 return -1;
352 });
353 }
354 getFolderContents(folderName) {
355 return __awaiter(this, void 0, void 0, function* () {
356 debug("getFolderContents");
357 const requestInit = {
358 body: `<?xml version="1.0"?>
359 <d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns" xmlns:ocs="http://open-collaboration-services.org/ns">
360 <d:prop>
361 <d:getlastmodified />
362 <d:getetag />
363 <d:getcontenttype />
364 <d:resourcetype />
365 <oc:fileid />
366 <oc:permissions />
367 <oc:size />
368 <d:getcontentlength />
369 <nc:has-preview />
370 <nc:mount-type />
371 <nc:is-encrypted />
372 <ocs:share-permissions />
373 <oc:tags />
374 <oc:favorite />
375 <oc:comments-unread />
376 <oc:owner-id />
377 <oc:owner-display-name />
378 <oc:share-types />
379 </d:prop>
380 </d:propfind>`,
381 method: "PROPFIND",
382 };
383 const url = `${this.webDAVUrl}${folderName}`;
384 const response = yield this.getHttpResponse(url, requestInit, [207], { description: "Folder get contents" });
385 const properties = yield this.getPropertiesFromWebDAVMultistatusResponse(response, "");
386 const folderContents = [];
387 // tslint:disable-next-line:no-empty
388 for (const prop of properties) {
389 let fileName = decodeURI(prop._href.substr(prop._href.indexOf(Client.webDavUrlPath) + 18));
390 if (fileName.endsWith("/")) {
391 fileName = fileName.slice(0, -1);
392 }
393 if ((url + "/").endsWith(prop._href)) {
394 continue;
395 }
396 const folderContentsEntry = {};
397 folderContentsEntry.lastmod = prop.getlastmodified;
398 folderContentsEntry.fileid = prop.fileid;
399 folderContentsEntry.basename = fileName.split("/").reverse()[0];
400 folderContentsEntry.filename = fileName;
401 if (prop.getcontenttype) {
402 folderContentsEntry.mime = prop.getcontenttype;
403 folderContentsEntry.size = prop.getcontentlength;
404 folderContentsEntry.type = "file";
405 }
406 else {
407 folderContentsEntry.type = "directory";
408 }
409 folderContents.push(folderContentsEntry);
410 }
411 // debug("folderContentsEntry $s", JSON.stringify(folderContents, null, 4));
412 return folderContents;
413 });
414 }
415 /**
416 * creates a folder and all parent folders in the path if they do not exist
417 * @param folderName name of the folder /folder/subfolder/subfolder
418 * @returns a folder object
419 */
420 createFolder(folderName) {
421 return __awaiter(this, void 0, void 0, function* () {
422 folderName = this.sanitizeFolderName(folderName);
423 debug("createFolder: folderName=%s", folderName);
424 const parts1 = folderName.split("/");
425 for (const p of parts1) {
426 if ((p) === "." || p === "..") {
427 throw new error_1.default(`Error creating folder, folder name "${folderName}" invalid`, "ERR_CREATE_FOLDER_INVALID_FOLDER_NAME");
428 }
429 }
430 let folder;
431 folder = yield this.getFolder(folderName);
432 if (folder) {
433 debug("createFolder: folder already available %O", folder.name);
434 return folder;
435 }
436 else {
437 // try to do a simple create with the complete path
438 try {
439 debug("createFolder: folder = %s", folderName);
440 yield this.createFolderInternal(folderName);
441 }
442 catch (e) {
443 // create all folders in the path
444 const parts = folderName.split("/");
445 parts.shift();
446 let folderPath = "";
447 debug("createFolder: parts = %O", parts);
448 for (const part of parts) {
449 debug("createFolder: part = %O", part);
450 folderPath += "/" + part;
451 folder = yield this.getFolder(folderPath);
452 if (folder === null) {
453 debug("createFolder: folder not available");
454 // folder not available
455 debug("createFolder: folder = %s", folderPath);
456 yield this.createFolderInternal(folderPath);
457 }
458 else {
459 debug("createFolder: folder already available %s", folderPath);
460 }
461 }
462 }
463 }
464 folder = yield this.getFolder(folderName);
465 if (folder) {
466 debug("createFolder: new folder %O", folder.name);
467 return folder;
468 }
469 else {
470 throw new error_1.default(`Error creating folder, folder name "${folderName}"
471 `, "ERR_CREATE_FOLDER_FAILED");
472 }
473 });
474 }
475 /**
476 * deletes a file
477 * @param fileName name of folder "/f1/f2/f3/x.txt"
478 */
479 deleteFile(fileName) {
480 return __awaiter(this, void 0, void 0, function* () {
481 const url = this.webDAVUrl + fileName;
482 debug("deleteFile %s", url);
483 const requestInit = {
484 method: "DELETE",
485 };
486 try {
487 yield this.getHttpResponse(url, requestInit, [204], { description: "File delete" });
488 }
489 catch (err) {
490 debug("Error in deleteFile %s %s %s", err.message, requestInit.method, url);
491 throw err;
492 }
493 });
494 }
495 /**
496 * deletes a folder
497 * @param folderName name of folder "/f1/f2/f3"
498 */
499 deleteFolder(folderName) {
500 return __awaiter(this, void 0, void 0, function* () {
501 folderName = this.sanitizeFolderName(folderName);
502 debug("deleteFolder:");
503 const folder = yield this.getFolder(folderName);
504 if (folder) {
505 yield this.deleteFile(folderName);
506 }
507 });
508 }
509 /**
510 * get a folder object from a path string
511 * @param folderName Name of the folder like "/company/branches/germany"
512 * @returns null if the folder does not exist or an folder object
513 */
514 getFolder(folderName) {
515 return __awaiter(this, void 0, void 0, function* () {
516 folderName = this.sanitizeFolderName(folderName);
517 debug("getFolder %s", folderName);
518 // return root folder
519 if (folderName === "/" || folderName === "") {
520 return new folder_1.default(this, "/", "", "");
521 }
522 try {
523 const stat = yield this.stat(folderName);
524 debug(": SUCCESS!!");
525 if (stat.type !== "file") {
526 return new folder_1.default(this, stat.filename.replace(/\\/g, "/"), stat.basename, stat.lastmod, stat.fileid);
527 }
528 else {
529 debug("getFolder: found object is file not a folder");
530 return null;
531 }
532 }
533 catch (e) {
534 debug("getFolder: exception occurred calling stat %O", e.message);
535 return null;
536 }
537 });
538 }
539 /**
540 * get a array of folders from a folder path string
541 * @param folderName Name of the folder like "/company/branches/germany"
542 * @returns array of folder objects
543 */
544 getSubFolders(folderName) {
545 return __awaiter(this, void 0, void 0, function* () {
546 debug("getSubFolders: folder %s", folderName);
547 const folders = [];
548 folderName = this.sanitizeFolderName(folderName);
549 const folderElements = yield this.Contents(folderName, true);
550 for (const folderElement of folderElements) {
551 debug("getSubFolders: adding subfolders %s", folderElement.filename);
552 folders.push(new folder_1.default(this, folderElement.filename.replace(/\\/g, "/"), folderElement.basename, folderElement.lastmod, folderElement.fileid));
553 }
554 return folders;
555 });
556 }
557 /**
558 * get files of a folder
559 * @param folderName Name of the folder like "/company/branches/germany"
560 * @returns array of file objects
561 */
562 getFiles(folderName) {
563 return __awaiter(this, void 0, void 0, function* () {
564 debug("getFiles: folder %s", folderName);
565 const files = [];
566 folderName = this.sanitizeFolderName(folderName);
567 const fileElements = yield this.Contents(folderName, false);
568 for (const folderElement of fileElements) {
569 debug("getFiles: adding file %s", folderElement.filename);
570 // debug("getFiles: adding file %O", folderElement);
571 files.push(new file_1.default(this, folderElement.filename.replace(/\\/g, "/"), folderElement.basename, folderElement.lastmod, folderElement.size, folderElement.mime, folderElement.fileid));
572 }
573 return files;
574 });
575 }
576 /**
577 * create a new file of overwrites an existing file
578 * @param fileName the file name /folder1/folder2/filename.txt
579 * @param data the buffer object
580 */
581 createFile(fileName, data) {
582 return __awaiter(this, void 0, void 0, function* () {
583 if (fileName.startsWith("./")) {
584 fileName = fileName.replace("./", "/");
585 }
586 const baseName = path_1.default.basename(fileName);
587 const folderName = path_1.default.dirname(fileName);
588 debug("createFile folder name %s base name %s", folderName, baseName);
589 // ensure that we have a folder
590 yield this.createFolder(folderName);
591 yield this.putFileContents(fileName, data);
592 let file;
593 file = yield this.getFile(fileName);
594 if (!file) {
595 throw new error_1.default(`Error creating file, file name "${fileName}"`, "ERR_CREATE_FILE_FAILED");
596 }
597 return file;
598 });
599 }
600 /**
601 * returns a nextcloud file object
602 * @param fileName the full file name /folder1/folder2/file.pdf
603 */
604 getFile(fileName) {
605 return __awaiter(this, void 0, void 0, function* () {
606 debug("getFile fileName = %s", fileName);
607 try {
608 const stat = yield this.stat(fileName);
609 debug(": SUCCESS!!");
610 if (stat.type === "file") {
611 return new file_1.default(this, stat.filename.replace(/\\/g, "/"), stat.basename, stat.lastmod, stat.size, stat.mime || "", stat.fileid || -1);
612 }
613 else {
614 debug("getFile: found object is a folder not a file");
615 return null;
616 }
617 }
618 catch (e) {
619 debug("getFile: exception occurred calling stat %O", e.message);
620 return null;
621 }
622 });
623 }
624 /**
625 * renames the file or moves it to an other location
626 * @param sourceFileName source file name
627 * @param targetFileName target file name
628 */
629 moveFile(sourceFileName, targetFileName) {
630 return __awaiter(this, void 0, void 0, function* () {
631 const url = this.webDAVUrl + sourceFileName;
632 const destinationUrl = this.webDAVUrl + targetFileName;
633 debug("moveFile from '%s' to '%s'", url, destinationUrl);
634 const requestInit = {
635 headers: new node_fetch_1.Headers({ Destination: destinationUrl }),
636 method: "MOVE",
637 };
638 try {
639 yield this.getHttpResponse(url, requestInit, [201], { description: "File move" });
640 }
641 catch (err) {
642 debug("Error in move file %s %s source: %s destination: %s", err.message, requestInit.method, url, destinationUrl);
643 throw new error_1.default("Error: moving file failed: source=" + sourceFileName + " target=" + targetFileName + " - " + err.message, "ERR_FILE_MOVE_FAILED");
644 }
645 const targetFile = yield this.getFile(targetFileName);
646 if (!targetFile) {
647 throw new error_1.default("Error: moving file failed: source=" + sourceFileName + " target=" + targetFileName, "ERR_FILE_MOVE_FAILED");
648 }
649 return targetFile;
650 });
651 }
652 /**
653 * renames the folder or moves it to an other location
654 * @param sourceFolderName source folder name
655 * @param tarName target folder name
656 */
657 moveFolder(sourceFolderName, tarName) {
658 return __awaiter(this, void 0, void 0, function* () {
659 const url = this.webDAVUrl + sourceFolderName;
660 const destinationUrl = this.webDAVUrl + tarName;
661 debug("moveFolder from '%s' to '%s'", url, destinationUrl);
662 const requestInit = {
663 headers: new node_fetch_1.Headers({ Destination: destinationUrl }),
664 method: "MOVE",
665 };
666 try {
667 yield this.getHttpResponse(url, requestInit, [201], { description: "Folder move" });
668 }
669 catch (err) {
670 debug("Error in move folder %s %s source: %s destination: %s", err.message, requestInit.method, url, destinationUrl);
671 throw new error_1.default("Error: moving folder failed: source=" + sourceFolderName + " target=" + tarName + " - " + err.message, "ERR_FOLDER_MOVE_FAILED");
672 }
673 const tar = yield this.getFolder(tarName);
674 if (!tar) {
675 throw new error_1.default("Error: moving folder failed: source=" + sourceFolderName + " target=" + tarName, "ERR_FOLDER_MOVE_FAILED");
676 }
677 return tar;
678 });
679 }
680 /**
681 * returns the content of a file
682 * @param fileName name of the file /d1/file1.txt
683 * @returns Buffer with file content
684 */
685 getContent(fileName) {
686 return __awaiter(this, void 0, void 0, function* () {
687 const url = this.webDAVUrl + fileName;
688 debug("getContent GET %s", url);
689 const requestInit = {
690 method: "GET",
691 };
692 let response;
693 try {
694 response = yield this.getHttpResponse(url, requestInit, [200], { description: "File get content" });
695 }
696 catch (err) {
697 debug("Error getContent %s - error %s", url, err.message);
698 throw err;
699 }
700 return Buffer.from(yield response.buffer());
701 });
702 }
703 /**
704 * returns the link to a file for downloading
705 * @param fileName name of the file /folder1/folder1.txt
706 * @returns url
707 */
708 getLink(fileName) {
709 debug("getLink of %s", fileName);
710 return this.webDAVUrl + fileName;
711 }
712 /**
713 * returns the url to the file in the nextcloud UI
714 * @param fileId the id of the file
715 */
716 getUILink(fileId) {
717 debug("getUILink of %s", fileId);
718 return `${this.nextcloudOrigin}/apps/files/?fileid=${fileId}`;
719 }
720 /**
721 * adds a tag to a file or folder
722 * if the tag does not exist, it is automatically created
723 * if the tag is created, the user must have damin privileges
724 * @param fileId the id of the file
725 * @param tagName the name of the tag
726 * @returns nothing
727 * @throws Error
728 */
729 addTagToFile(fileId, tagName) {
730 return __awaiter(this, void 0, void 0, function* () {
731 debug("addTagToFile file:%s tag:%s", fileId, tagName);
732 const tag = yield this.createTag(tagName);
733 if (!tag.canAssign) {
734 throw new error_1.default(`Error: No permission to assign tag "${tagName}" to file. Tag is not assignable`, "ERR_TAG_NOT_ASSIGNABLE");
735 }
736 const addTagBody = {
737 canAssign: tag.canAssign,
738 id: tag.id,
739 name: tag.name,
740 userAssignable: tag.assignable,
741 userVisible: tag.visible,
742 };
743 const requestInit = {
744 body: JSON.stringify(addTagBody, null, 4),
745 headers: new node_fetch_1.Headers({ "Content-Type": "application/json" }),
746 method: "PUT",
747 };
748 yield this.getHttpResponse(`${this.nextcloudOrigin}/remote.php/dav/systemtags-relations/files/${fileId}/${tag.id}`, requestInit, [201, 409], { description: "File add tag" }); // created or conflict
749 });
750 }
751 // ***************************************************************************************
752 // activity
753 // ***************************************************************************************
754 /*
755 to be refactored to eventing
756
757 public async getActivities(): Promise<string[]> {
758 const result: string[] = [];
759 const requestInit: RequestInit = {
760 headers: new Headers({ "ocs-apirequest": "true" }),
761 method: "GET",
762 };
763
764 const response: Response = await this.getHttpResponse(
765 this.nextcloudOrigin + "/ocs/v2.php/apps/activity/api/v2/activity/files?format=json&previews=false&since=97533",
766 requestInit,
767 [200],
768 { description: "Activities get" });
769
770 const responseObject: any = await response.json();
771 // @todo
772
773 for (const res of responseObject.ocs.data) {
774 debug(JSON.stringify({
775 acivityId: res.activity_id,
776 objects: res.objects,
777 type: res.type,
778 }, null, 4));
779 }
780
781 // debug("getActivities: responseObject %s", JSON.stringify(responseObject, null, 4));
782
783 return result;
784 }
785*/
786 // ***************************************************************************************
787 // comments
788 // ***************************************************************************************
789 /**
790 * adds a comment to a file
791 * @param fileId the id of the file
792 * @param comment the comment to be added to the file
793 */
794 addCommentToFile(fileId, comment) {
795 return __awaiter(this, void 0, void 0, function* () {
796 debug("addCommentToFile file:%s comment:%s", fileId, comment);
797 const addCommentBody = {
798 actorType: "users",
799 message: comment,
800 objectType: "files",
801 verb: "comment",
802 };
803 const requestInit = {
804 body: JSON.stringify(addCommentBody, null, 4),
805 headers: new node_fetch_1.Headers({ "Content-Type": "application/json" }),
806 method: "POST",
807 };
808 yield this.getHttpResponse(`${this.nextcloudOrigin}/remote.php/dav/comments/files/${fileId}`, requestInit, [201], { description: "File add comment" }); // created
809 });
810 }
811 /**
812 * returns comments of a file / folder
813 * @param fileId the id of the file / folder
814 * @param top number of comments to return
815 * @param skip the offset
816 * @returns array of comment strings
817 * @throws Exception
818 */
819 getFileComments(fileId, top, skip) {
820 return __awaiter(this, void 0, void 0, function* () {
821 debug("getFileComments fileId:%s", fileId);
822 if (!top) {
823 top = 30;
824 }
825 if (!skip) {
826 skip = 0;
827 }
828 const requestInit = {
829 body: `<?xml version="1.0" encoding="utf-8" ?>
830 <oc:filter-comments xmlns:D="DAV:" xmlns:oc="http://owncloud.org/ns">
831 <oc:limit>${top}</oc:limit>
832 <oc:offset>${skip}</oc:offset>
833 </oc:filter-comments>`,
834 method: "REPORT",
835 };
836 const response = yield this.getHttpResponse(`${this.nextcloudOrigin}/remote.php/dav/comments/files/${fileId}`, requestInit, [207], { description: "File get comments" });
837 const properties = yield this.getPropertiesFromWebDAVMultistatusResponse(response, "");
838 const comments = [];
839 for (const prop of properties) {
840 comments.push(prop.message);
841 }
842 return comments;
843 });
844 }
845 /**
846 * returns system information about the nextcloud server and the nextcloud client
847 */
848 getSystemInfo() {
849 return __awaiter(this, void 0, void 0, function* () {
850 const requestInit = {
851 headers: new node_fetch_1.Headers({ "ocs-apirequest": "true" }),
852 method: "GET",
853 };
854 const response = yield this.getHttpResponse(this.nextcloudOrigin + "/ocs/v2.php/apps/serverinfo/api/v1/info?format=json", requestInit, [200], { description: "SystemInfo get" });
855 const rawResult = yield response.json();
856 // validate the raw result
857 let version;
858 if (rawResult.ocs &&
859 rawResult.ocs.data &&
860 rawResult.ocs.data.nextcloud &&
861 rawResult.ocs.data.nextcloud.system &&
862 rawResult.ocs.data.nextcloud.system.version) {
863 version = rawResult.ocs.data.nextcloud.system.version;
864 }
865 else {
866 throw new error_1.default("Fatal Error: nextcloud system version missing", "ERR_SYSTEM_INFO_MISSING_DATA");
867 }
868 const result = {
869 nextcloud: {
870 system: {
871 version,
872 },
873 },
874 nextcloudClient: {
875 version: require("../package.json").version,
876 },
877 };
878 return result;
879 });
880 }
881 // ***************************************************************************************
882 // user management
883 // ***************************************************************************************
884 /**
885 * returns users
886 */
887 getUserIDs() {
888 return __awaiter(this, void 0, void 0, function* () {
889 const requestInit = {
890 headers: new node_fetch_1.Headers({ "OCS-APIRequest": "true", "Accept": "application/json" }),
891 method: "GET",
892 };
893 const response = yield this.getHttpResponse(
894 // ?perPage=1 page=
895 this.nextcloudOrigin + "/ocs/v1.php/cloud/users", requestInit, [200], { description: "Users get" });
896 const rawResult = yield response.json();
897 let users = [];
898 if (rawResult.ocs &&
899 rawResult.ocs.data &&
900 rawResult.ocs.data.users) {
901 users = rawResult.ocs.data.users;
902 }
903 return users;
904 });
905 }
906 createUser(options) {
907 return __awaiter(this, void 0, void 0, function* () {
908 const requestInit = {
909 body: JSON.stringify(options, null, 4),
910 headers: new node_fetch_1.Headers({
911 "Content-Type": "application/x-www-form-urlencoded",
912 "OCS-APIRequest": "true",
913 }),
914 method: "POST",
915 };
916 debug("request body: ", requestInit.body);
917 const response = yield this.getHttpResponse(this.nextcloudOrigin + "/ocs/v1.php/cloud/users", requestInit, [200], { description: "User create" });
918 const rawResult = yield response.json();
919 debug(rawResult);
920 });
921 }
922 // ***************************************************************************************
923 // shares
924 // https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-share-api.html
925 // ***************************************************************************************
926 /**
927 * create a new share
928 */
929 createShare(options) {
930 return __awaiter(this, void 0, void 0, function* () {
931 const shareRequest = share_1.default.createShareRequestBody(options);
932 debug(shareRequest);
933 const headers = {
934 "Accept": "application/json",
935 "Content-Type": "application/json;charset=UTF-8",
936 "OCS-APIRequest": "true",
937 };
938 const requestInit = {
939 body: shareRequest,
940 headers: new node_fetch_1.Headers(headers),
941 method: "POST",
942 };
943 const url = this.nextcloudOrigin + "/ocs/v2.php/apps/files_sharing/api/v1/shares";
944 // try {
945 const response = yield this.getHttpResponse(url, requestInit, [200], { description: "Share create" });
946 const rawResult = yield response.json();
947 debug(rawResult);
948 return share_1.default.getShare(this, rawResult.ocs.data.id);
949 /* } catch (e) {
950 debug("result " + e.message);
951 debug("requestInit ", JSON.stringify(requestInit, null, 4));
952 debug("headers " + JSON.stringify(headers, null, 4));
953 debug("url ", url);
954 throw e;
955 } */
956 });
957 }
958 /**
959 * update a new share
960 */
961 updateShare(shareId, body) {
962 return __awaiter(this, void 0, void 0, function* () {
963 debug("updateShare body ", body);
964 const headers = {
965 "Accept": "application/json",
966 "Content-Type": "application/json;charset=UTF-8",
967 "OCS-APIRequest": "true",
968 };
969 const requestInit = {
970 body: JSON.stringify(body, null, 4),
971 headers: new node_fetch_1.Headers(headers),
972 method: "PUT",
973 };
974 const url = this.nextcloudOrigin + "/ocs/v2.php/apps/files_sharing/api/v1/shares/" + shareId;
975 yield this.getHttpResponse(url, requestInit, [200], { description: "Share update" });
976 });
977 }
978 /**
979 * get share information
980 * @param shareId
981 */
982 getShare(shareId) {
983 return __awaiter(this, void 0, void 0, function* () {
984 const headers = {
985 "Accept": "application/json",
986 "OCS-APIRequest": "true",
987 };
988 const requestInit = {
989 headers: new node_fetch_1.Headers(headers),
990 method: "GET",
991 };
992 const url = this.nextcloudOrigin + "/ocs/v2.php/apps/files_sharing/api/v1/shares/" + shareId;
993 const response = yield this.getHttpResponse(url, requestInit, [200], { description: "Share get" });
994 const rawResult = yield response.json();
995 return rawResult;
996 /*
997 } catch (e) {
998 debug("result " + e.message);
999 debug("requestInit ", JSON.stringify(requestInit, null, 4));
1000 debug("headers " + JSON.stringify(headers, null, 4));
1001 debug("url ", url);
1002 throw e;
1003 }
1004 */
1005 });
1006 }
1007 /**
1008 * get share information
1009 * @param shareId
1010 */
1011 deleteShare(shareId) {
1012 return __awaiter(this, void 0, void 0, function* () {
1013 const headers = {
1014 "Accept": "application/json",
1015 "OCS-APIRequest": "true",
1016 };
1017 const requestInit = {
1018 headers: new node_fetch_1.Headers(headers),
1019 method: "DELETE",
1020 };
1021 const url = this.nextcloudOrigin + "/ocs/v2.php/apps/files_sharing/api/v1/shares/" + shareId;
1022 const response = yield this.getHttpResponse(url, requestInit, [200], { description: "Share delete" });
1023 });
1024 }
1025 // ***************************************************************************************
1026 // private methods
1027 // ***************************************************************************************
1028 /**
1029 * asserts valid xml
1030 * asserts multistatus response
1031 * asserts that a href is available in the multistatus response
1032 * asserts propstats and prop
1033 * @param response the http response
1034 * @param href get only properties that match the href
1035 * @returns array of properties
1036 * @throws GeneralError
1037 */
1038 getPropertiesFromWebDAVMultistatusResponse(response, href) {
1039 return __awaiter(this, void 0, void 0, function* () {
1040 const responseContentType = response.headers.get("Content-Type");
1041 if (!responseContentType) {
1042 throw new error_1.default("Response content type expected", "ERR_RESPONSE_WITHOUT_CONTENT_TYPE_HEADER");
1043 }
1044 if (responseContentType.indexOf("application/xml") === -1) {
1045 throw new error_1.default("XML response content type expected", "ERR_XML_RESPONSE_CONTENT_TYPE_EXPECTED");
1046 }
1047 const xmlBody = yield response.text();
1048 if (fast_xml_parser_1.default.validate(xmlBody) !== true) {
1049 throw new error_1.default(`The response is not valid XML: ${xmlBody}`, "ERR_RESPONSE_NOT_INVALID_XML");
1050 }
1051 const options = {
1052 ignoreNameSpace: true,
1053 };
1054 const body = fast_xml_parser_1.default.parse(xmlBody, options);
1055 // ensure that we have a multistatus response
1056 if (!body.multistatus || !body.multistatus.response) {
1057 throw new error_1.default(`The response is is not a WebDAV multistatus response`, "ERR_RESPONSE_NO_MULTISTATUS_XML");
1058 }
1059 // ensure that response is always an array
1060 if (body.multistatus.response.href || body.multistatus.response.propstat) {
1061 body.multistatus.response = new Array(body.multistatus.response);
1062 }
1063 /*
1064 if (body.multistatus.response.propstat) {
1065 body.multistatus.response = [body.multistatus.response];
1066 }
1067 */
1068 const responseProperties = [];
1069 for (const res of body.multistatus.response) {
1070 if (!res.href) {
1071 throw new error_1.default(`The mulitstatus response must have a href`, "ERR_RESPONSE_MISSING_HREF_MULTISTATUS");
1072 }
1073 if (!res.propstat) {
1074 throw new error_1.default(`The mulitstatus response must have a "propstat" container`, "ERR_RESPONSE_MISSING_PROPSTAT");
1075 }
1076 let propStats = res.propstat;
1077 // ensure an array
1078 if (res.propstat.status || res.propstat.prop) {
1079 propStats = [res.propstat];
1080 }
1081 for (const propStat of propStats) {
1082 if (!propStat.status) {
1083 throw new error_1.default(`The propstat must have a "status"`, "ERR_RESPONSE_MISSING_PROPSTAT_STATUS");
1084 }
1085 if (propStat.status === "HTTP/1.1 200 OK") {
1086 if (!propStat.prop) {
1087 throw new error_1.default(`The propstat must have a "prop"`, "ERR_RESPONSE_MISSING_PROPSTAT_PROP");
1088 }
1089 const property = propStat.prop;
1090 property._href = res.href;
1091 responseProperties.push(property);
1092 }
1093 }
1094 // }
1095 }
1096 return responseProperties;
1097 });
1098 }
1099 /**
1100 * nextcloud creates a csrf token and stores it in the html header attribute
1101 * data-requesttoken
1102 * this function is currently not used
1103 * @returns the csrf token / requesttoken
1104 */
1105 /*
1106 private async getCSRFToken(): Promise<string> {
1107
1108 const requestInit: RequestInit = {
1109 method: "GET",
1110 };
1111
1112 const response: Response = await this.getHttpResponse(
1113 this.nextcloudOrigin,
1114 requestInit,
1115 [200],
1116 { description: "CSER token get" });
1117
1118 const html = await response.text();
1119
1120 const requestToken: string = html.substr(html.indexOf("data-requesttoken=") + 19, 89);
1121 debug("getCSRFToken %s", requestToken);
1122 return requestToken;
1123 }
1124 */
1125 getHttpResponse(url, requestInit, expectedHttpStatusCode, context) {
1126 return __awaiter(this, void 0, void 0, function* () {
1127 if (!requestInit.headers) {
1128 requestInit.headers = new node_fetch_1.Headers();
1129 }
1130 /* istanbul ignore else */
1131 if (this.fakeServer) {
1132 return yield this.fakeServer.getFakeHttpResponse(url, requestInit, expectedHttpStatusCode, context);
1133 }
1134 else {
1135 return yield this.httpClient.getHttpResponse(url, requestInit, expectedHttpStatusCode, context);
1136 }
1137 });
1138 }
1139 /**
1140 * get contents array of a folder
1141 * @param folderName Name of the folder like "/company/branches/germany"
1142 * @param folderIndicator true if folders are requested otherwise files
1143 * @returns array of folder contents meta data
1144 */
1145 Contents(folderName, folderIndicator) {
1146 return __awaiter(this, void 0, void 0, function* () {
1147 debug("Contents: folder %s", folderName);
1148 const folders = [];
1149 folderName = this.sanitizeFolderName(folderName);
1150 const resultArray = [];
1151 if (folderIndicator === true) {
1152 debug("Contents: get folders");
1153 }
1154 else {
1155 debug("Contents: get files");
1156 }
1157 try {
1158 const folderContentsArray = yield this.getFolderContents(folderName);
1159 // debug("###########################");
1160 // debug("$s", JSON.stringify(folderContentsArray, null, 4));
1161 // debug("###########################");
1162 yield this.getFolderContents(folderName);
1163 for (const folderElement of folderContentsArray) {
1164 if (folderElement.type === "directory") {
1165 // if (folderIndicator === true) {
1166 resultArray.push(folderElement);
1167 // }
1168 }
1169 else {
1170 // if (folderIndicator === false) {
1171 debug("Contents folder element file %O ", folderElement);
1172 resultArray.push(folderElement);
1173 // }
1174 }
1175 }
1176 }
1177 catch (e) {
1178 debug("Contents: exception occurred %s", e.message);
1179 }
1180 return resultArray;
1181 });
1182 }
1183 sanitizeFolderName(folderName) {
1184 if (folderName[0] !== "/") {
1185 folderName = "/" + folderName;
1186 }
1187 // remove trailing "/" es
1188 folderName = folderName.replace(/\/+$/, "");
1189 if (folderName === "") {
1190 folderName = "/";
1191 }
1192 return folderName;
1193 }
1194 getTagIdFromHref(href) {
1195 return parseInt(href.split("/")[href.split("/").length - 1], 10);
1196 }
1197 createFolderInternal(folderName) {
1198 return __awaiter(this, void 0, void 0, function* () {
1199 const url = this.webDAVUrl + folderName;
1200 debug("createFolderInternal %s", url);
1201 const requestInit = {
1202 method: "MKCOL",
1203 };
1204 try {
1205 yield this.getHttpResponse(url, requestInit, [201], { description: "Folder create" });
1206 }
1207 catch (err) {
1208 debug("Error in createFolderInternal %s %s %s %s", err.message, requestInit.method, url);
1209 throw err;
1210 }
1211 });
1212 }
1213 stat(fileName) {
1214 return __awaiter(this, void 0, void 0, function* () {
1215 const url = this.webDAVUrl + fileName;
1216 debug("stat %s", url);
1217 const requestInit = {
1218 body: `<?xml version="1.0"?>
1219 <d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
1220 <d:prop>
1221 <d:getlastmodified />
1222 <d:getetag />
1223 <d:getcontenttype />
1224 <d:resourcetype />
1225 <oc:fileid />
1226 <oc:permissions />
1227 <oc:size />
1228 <d:getcontentlength />
1229 <nc:has-preview />
1230 <oc:favorite />
1231 <oc:comments-unread />
1232 <oc:owner-display-name />
1233 <oc:share-types />
1234 </d:prop>
1235 </d:propfind>`,
1236 headers: new node_fetch_1.Headers({ Depth: "0" }),
1237 method: "PROPFIND",
1238 };
1239 let response;
1240 try {
1241 response = yield this.getHttpResponse(url, requestInit, [207], { description: "File/Folder get details" });
1242 }
1243 catch (err) {
1244 debug("Error in stat %s %s %s %s", err.message, requestInit.method, url);
1245 throw err;
1246 }
1247 const properties = yield this.getPropertiesFromWebDAVMultistatusResponse(response, "");
1248 let resultStat = null;
1249 for (const prop of properties) {
1250 resultStat = {
1251 basename: path_1.basename(fileName),
1252 fileid: prop.fileid,
1253 filename: fileName,
1254 lastmod: prop.getlastmodified,
1255 type: "file",
1256 };
1257 if (prop.getcontentlength) {
1258 resultStat.size = prop.getcontentlength;
1259 }
1260 else {
1261 resultStat.type = "directory";
1262 }
1263 if (prop.getcontenttype) {
1264 resultStat.mime = prop.getcontenttype;
1265 }
1266 }
1267 if (!resultStat) {
1268 debug("Error: response %s", JSON.stringify(properties, null, 4));
1269 throw new error_1.default("Error getting status information from : " + url, "ERR_STAT");
1270 }
1271 return resultStat;
1272 });
1273 }
1274 putFileContents(fileName, data) {
1275 return __awaiter(this, void 0, void 0, function* () {
1276 const url = this.webDAVUrl + fileName;
1277 debug("putFileContents %s", url);
1278 const requestInit = {
1279 body: data,
1280 method: "PUT",
1281 };
1282 let response;
1283 response = yield this.getHttpResponse(url, requestInit, [201, 204], { description: "File save content" });
1284 return response;
1285 });
1286 }
1287}
1288exports.default = Client;
1289exports.Client = Client;
1290Client.webDavUrlPath = "/remote.php/webdav";