UNPKG

nextcloud-node-client

Version:

Nextcloud client API for node.js applications

1,161 lines (1,151 loc) 54.9 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; result["default"] = mod; return result; }; Object.defineProperty(exports, "__esModule", { value: true }); // tslint:disable-next-line:no-var-requires require("dotenv").config(); const debug_1 = __importDefault(require("debug")); const fast_xml_parser_1 = __importDefault(require("fast-xml-parser")); const node_fetch_1 = require("node-fetch"); const path_1 = __importStar(require("path")); const environment_1 = __importDefault(require("./environment")); const environmentVcapServices_1 = __importDefault(require("./environmentVcapServices")); const error_1 = __importDefault(require("./error")); exports.ClientError = error_1.default; const fakeServer_1 = __importDefault(require("./fakeServer")); exports.FakeServer = fakeServer_1.default; const file_1 = __importDefault(require("./file")); exports.File = file_1.default; const fileSystemElement_1 = __importDefault(require("./fileSystemElement")); exports.FileSystemElement = fileSystemElement_1.default; const folder_1 = __importDefault(require("./folder")); exports.Folder = folder_1.default; const httpClient_1 = require("./httpClient"); const requestResponseLogEntry_1 = __importDefault(require("./requestResponseLogEntry")); exports.RequestResponseLogEntry = requestResponseLogEntry_1.default; const server_1 = __importDefault(require("./server")); exports.Server = server_1.default; const share_1 = __importStar(require("./share")); exports.SharePermission = share_1.SharePermission; const tag_1 = __importDefault(require("./tag")); exports.Tag = tag_1.default; const debug = debug_1.default("NCClient"); /** * The nextcloud client is the root object to access the remote api of the nextcloud server.<br> */ class Client { /** * Creates a new instance of a nextcloud client.<br/> * Use the server to provide server connectivity information to the client.<br/> * (The FakeServer is only used for testing and code coverage)<br/><br/> * If the server is not provided the client tries to find the connectivity information * in the environment.<br/> * If a <b>VCAP_SERVICES</b> environment variable is available, the client tries to find * a service with the name <b>"nextcloud"</b> in the user-provides-services section.<br/> * If no VCAP_SERVICES are available, the client uses the following variables * from the envirnonment for the connectivity:<br/> * <ul> * <li>NEXTCLOUD_URL - the WebDAV url of the nextcloud server</li> * <li>NEXTCLOUD_USERNAME - the user name</li> * <li>NEXTCLOUD_PASSWORD - the application password</li> * </ul> * @param server optional server information to connection to a nextcloud server * @constructor */ constructor(server) { this.logRequestResponse = false; debug("constructor"); this.nextcloudOrigin = ""; this.nextcloudAuthHeader = ""; this.nextcloudRequestToken = ""; this.webDAVUrl = ""; // if no server is provided, try to get a server from VCAP_S environment "nextcloud" instance // If no VCAP_S environment exists try from environment if (!server) { try { const env = new environmentVcapServices_1.default("nextcloud"); server = env.getServer(); } catch (e) { const env = new environment_1.default(); server = env.getServer(); } } if (server instanceof server_1.default) { this.proxy = server.proxy; debug("constructor: webdav url %s", server.url); if (server.url.indexOf(Client.webDavUrlPath) === -1) { // not a valid nextcloud url 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"); } this.nextcloudOrigin = server.url.substr(0, server.url.indexOf(Client.webDavUrlPath)); debug("constructor: nextcloud url %s", this.nextcloudOrigin); this.nextcloudAuthHeader = "Basic " + Buffer.from(server.basicAuth.username + ":" + server.basicAuth.password).toString("base64"); this.nextcloudRequestToken = ""; if (server.url.slice(-1) === "/") { this.webDAVUrl = server.url.slice(0, -1); } else { this.webDAVUrl = server.url; } this.logRequestResponse = server.logRequestResponse; const options = { authorizationHeader: this.nextcloudAuthHeader, logRequestResponse: this.logRequestResponse, origin: this.nextcloudOrigin, proxy: this.proxy, }; this.httpClient = new httpClient_1.HttpClient(options); } if (server instanceof fakeServer_1.default) { this.fakeServer = server; this.webDAVUrl = "https://fake.server" + Client.webDavUrlPath; } } /** * returns the used and free quota of the nextcloud account */ getQuota() { return __awaiter(this, void 0, void 0, function* () { debug("getQuota"); const requestInit = { method: "PROPFIND", }; const response = yield this.getHttpResponse(this.webDAVUrl + "/", requestInit, [207], { description: "Client get quota" }); const properties = yield this.getPropertiesFromWebDAVMultistatusResponse(response, Client.webDavUrlPath + "/"); let quota = null; for (const prop of properties) { if (prop["quota-available-bytes"]) { quota = { available: "unlimited", used: prop["quota-used-bytes"], }; if (prop["quota-available-bytes"] > 0) { quota.available = prop["quota-available-bytes"]; } } } if (!quota) { debug("Error, quota not available: %s ", JSON.stringify(properties, null, 4)); throw new error_1.default(`Error, quota not available`, "ERR_QUOTA_NOT_AVAILABLE"); } debug("getQuota = %O", quota); return quota; }); } // *************************************************************************************** // tags // *************************************************************************************** /** * creates a new tag, if not already existing * this function will fail with http 403 if the user does not have admin privileges * @param tagName the name of the tag * @returns tagId */ createTag(tagName) { return __awaiter(this, void 0, void 0, function* () { debug("createTag"); let tag; // is the tag already existing? tag = yield this.getTagByName(tagName); if (tag) { return tag; } // tag does not exist, create tag const requestInit = { body: `{ "name": "${tagName}", "userVisible": true, "userAssignable": true, "canAssign": true }`, headers: new node_fetch_1.Headers({ "Content-Type": "application/json" }), method: "POST", }; const response = yield this.getHttpResponse(this.nextcloudOrigin + "/remote.php/dav/systemtags/", requestInit, [201], { description: "Tag create" }); const tagString = response.headers.get("Content-Location"); debug("createTag new tagId %s, tagName %s", tagString, tagName); if (tagString === "" || tagString === null) { throw new error_1.default(`Error, tag with name '${tagName}' could not be created`, "ERR_TAG_CREATE_FAILED"); } // the number id of the tag is the last element in the id (path) const tagId = this.getTagIdFromHref(tagString); tag = new tag_1.default(this, tagId, tagName, true, true, true); return tag; }); } /** * returns a tag identified by the name or null if not found * @param tagName the name of the tag * @returns tag or null */ getTagByName(tagName) { return __awaiter(this, void 0, void 0, function* () { debug("getTag"); const tags = yield this.getTags(); for (const tag of tags) { if (tag.name === tagName) { return tag; } } return null; }); } /** * returns a tag identified by the id or null if not found * @param tagId the id of the tag * @returns tag or null */ getTagById(tagId) { return __awaiter(this, void 0, void 0, function* () { debug("getTagById"); const tags = yield this.getTags(); for (const tag of tags) { if (tag.id === tagId) { return tag; } } return null; }); } /** * deletes the tag by id * this function will fail with http 403 if the user does not have admin privileges * @param tagId the id of the tag like "/remote.php/dav/systemtags/234" */ deleteTag(tagId) { return __awaiter(this, void 0, void 0, function* () { debug("deleteTag tagId: $s", tagId); const requestInit = { method: "DELETE", }; const response = yield this.getHttpResponse(`${this.nextcloudOrigin}/remote.php/dav/systemtags/${tagId}`, requestInit, [204, 404], { description: "Tag delete" }); }); } /** * deletes all visible assignable tags * @throws Error */ deleteAllTags() { return __awaiter(this, void 0, void 0, function* () { debug("deleteAllTags"); const tags = yield this.getTags(); for (const tag of tags) { // debug("deleteAllTags tag: %O", tag); yield tag.delete(); } }); } /** * returns a list of tags * @returns array of tags */ getTags() { return __awaiter(this, void 0, void 0, function* () { debug("getTags PROPFIND %s", this.nextcloudOrigin + "/remote.php/dav/systemtags/"); const requestInit = { body: `<?xml version="1.0"?> <d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns"> <d:prop> <oc:id /> <oc:display-name /> <oc:user-visible /> <oc:user-assignable /> <oc:can-assign /> </d:prop> </d:propfind>`, method: "PROPFIND", }; const relUrl = `/remote.php/dav/systemtags/`; const response = yield this.getHttpResponse(this.nextcloudOrigin + relUrl, requestInit, [207], { description: "Tags get" }); const properties = yield this.getPropertiesFromWebDAVMultistatusResponse(response, relUrl + "/*"); const tags = []; for (const prop of properties) { tags.push(new tag_1.default(this, this.getTagIdFromHref(prop._href), prop["display-name"], prop["user-visible"], prop["user-assignable"], prop["can-assign"])); } return tags; }); } /** * returns the list of tag names and the tag ids * @param fileId the id of the file */ getTagsOfFile(fileId) { return __awaiter(this, void 0, void 0, function* () { debug("getTagsOfFile"); const requestInit = { body: `<?xml version="1.0"?> <d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns"> <d:prop> <oc:id /> <oc:display-name /> <oc:user-visible /> <oc:user-assignable /> <oc:can-assign /> </d:prop> </d:propfind>`, method: "PROPFIND", }; const relUrl = `/remote.php/dav/systemtags-relations/files/${fileId}`; const response = yield this.getHttpResponse(`${this.nextcloudOrigin}${relUrl}`, requestInit, [207], { description: "File get tags" }); const properties = yield this.getPropertiesFromWebDAVMultistatusResponse(response, relUrl + "/*"); const tagMap = new Map(); for (const prop of properties) { tagMap.set(prop["display-name"], prop.id); } debug("tags of file %O", tagMap); return tagMap; }); } /** * removes the tag from the file * @param fileId the file id * @param tagId the tag id */ removeTagOfFile(fileId, tagId) { return __awaiter(this, void 0, void 0, function* () { debug("removeTagOfFile tagId: $s fileId:", tagId, fileId); const requestInit = { method: "DELETE", }; const response = yield this.getHttpResponse(`${this.nextcloudOrigin}/remote.php/dav/systemtags-relations/files/${fileId}/${tagId}`, requestInit, [204, 404], { description: "File remove tag" }); return; }); } /** * returns the id of the file or -1 of not found * @returns id of the file or -1 if not found */ getFileId(fileUrl) { return __awaiter(this, void 0, void 0, function* () { debug("getFileId"); const requestInit = { body: ` <d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns"> <d:prop> <oc:fileid /> </d:prop> </d:propfind>`, method: "PROPFIND", }; const response = yield this.getHttpResponse(fileUrl, requestInit, [207], { description: "File get id" }); const properties = yield this.getPropertiesFromWebDAVMultistatusResponse(response, ""); for (const prop of properties) { if (prop.fileid) { return prop.fileid; } } debug("getFileId no file id found for %s", fileUrl); return -1; }); } getFolderContents(folderName) { return __awaiter(this, void 0, void 0, function* () { debug("getFolderContents"); const requestInit = { body: `<?xml version="1.0"?> <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"> <d:prop> <d:getlastmodified /> <d:getetag /> <d:getcontenttype /> <d:resourcetype /> <oc:fileid /> <oc:permissions /> <oc:size /> <d:getcontentlength /> <nc:has-preview /> <nc:mount-type /> <nc:is-encrypted /> <ocs:share-permissions /> <oc:tags /> <oc:favorite /> <oc:comments-unread /> <oc:owner-id /> <oc:owner-display-name /> <oc:share-types /> </d:prop> </d:propfind>`, method: "PROPFIND", }; const url = `${this.webDAVUrl}${folderName}`; const response = yield this.getHttpResponse(url, requestInit, [207], { description: "Folder get contents" }); const properties = yield this.getPropertiesFromWebDAVMultistatusResponse(response, ""); const folderContents = []; // tslint:disable-next-line:no-empty for (const prop of properties) { let fileName = decodeURI(prop._href.substr(prop._href.indexOf(Client.webDavUrlPath) + 18)); if (fileName.endsWith("/")) { fileName = fileName.slice(0, -1); } if ((url + "/").endsWith(prop._href)) { continue; } const folderContentsEntry = {}; folderContentsEntry.lastmod = prop.getlastmodified; folderContentsEntry.fileid = prop.fileid; folderContentsEntry.basename = fileName.split("/").reverse()[0]; folderContentsEntry.filename = fileName; if (prop.getcontenttype) { folderContentsEntry.mime = prop.getcontenttype; folderContentsEntry.size = prop.getcontentlength; folderContentsEntry.type = "file"; } else { folderContentsEntry.type = "directory"; } folderContents.push(folderContentsEntry); } // debug("folderContentsEntry $s", JSON.stringify(folderContents, null, 4)); return folderContents; }); } /** * creates a folder and all parent folders in the path if they do not exist * @param folderName name of the folder /folder/subfolder/subfolder * @returns a folder object */ createFolder(folderName) { return __awaiter(this, void 0, void 0, function* () { folderName = this.sanitizeFolderName(folderName); debug("createFolder: folderName=%s", folderName); const parts1 = folderName.split("/"); for (const p of parts1) { if ((p) === "." || p === "..") { throw new error_1.default(`Error creating folder, folder name "${folderName}" invalid`, "ERR_CREATE_FOLDER_INVALID_FOLDER_NAME"); } } let folder; folder = yield this.getFolder(folderName); if (folder) { debug("createFolder: folder already available %O", folder.name); return folder; } else { // try to do a simple create with the complete path try { debug("createFolder: folder = %s", folderName); yield this.createFolderInternal(folderName); } catch (e) { // create all folders in the path const parts = folderName.split("/"); parts.shift(); let folderPath = ""; debug("createFolder: parts = %O", parts); for (const part of parts) { debug("createFolder: part = %O", part); folderPath += "/" + part; folder = yield this.getFolder(folderPath); if (folder === null) { debug("createFolder: folder not available"); // folder not available debug("createFolder: folder = %s", folderPath); yield this.createFolderInternal(folderPath); } else { debug("createFolder: folder already available %s", folderPath); } } } } folder = yield this.getFolder(folderName); if (folder) { debug("createFolder: new folder %O", folder.name); return folder; } else { throw new error_1.default(`Error creating folder, folder name "${folderName}" `, "ERR_CREATE_FOLDER_FAILED"); } }); } /** * deletes a file * @param fileName name of folder "/f1/f2/f3/x.txt" */ deleteFile(fileName) { return __awaiter(this, void 0, void 0, function* () { const url = this.webDAVUrl + fileName; debug("deleteFile %s", url); const requestInit = { method: "DELETE", }; try { yield this.getHttpResponse(url, requestInit, [204], { description: "File delete" }); } catch (err) { debug("Error in deleteFile %s %s %s", err.message, requestInit.method, url); throw err; } }); } /** * deletes a folder * @param folderName name of folder "/f1/f2/f3" */ deleteFolder(folderName) { return __awaiter(this, void 0, void 0, function* () { folderName = this.sanitizeFolderName(folderName); debug("deleteFolder:"); const folder = yield this.getFolder(folderName); if (folder) { yield this.deleteFile(folderName); } }); } /** * get a folder object from a path string * @param folderName Name of the folder like "/company/branches/germany" * @returns null if the folder does not exist or an folder object */ getFolder(folderName) { return __awaiter(this, void 0, void 0, function* () { folderName = this.sanitizeFolderName(folderName); debug("getFolder %s", folderName); // return root folder if (folderName === "/" || folderName === "") { return new folder_1.default(this, "/", "", ""); } try { const stat = yield this.stat(folderName); debug(": SUCCESS!!"); if (stat.type !== "file") { return new folder_1.default(this, stat.filename.replace(/\\/g, "/"), stat.basename, stat.lastmod, stat.fileid); } else { debug("getFolder: found object is file not a folder"); return null; } } catch (e) { debug("getFolder: exception occurred calling stat %O", e.message); return null; } }); } /** * get a array of folders from a folder path string * @param folderName Name of the folder like "/company/branches/germany" * @returns array of folder objects */ getSubFolders(folderName) { return __awaiter(this, void 0, void 0, function* () { debug("getSubFolders: folder %s", folderName); const folders = []; folderName = this.sanitizeFolderName(folderName); const folderElements = yield this.Contents(folderName, true); for (const folderElement of folderElements) { debug("getSubFolders: adding subfolders %s", folderElement.filename); folders.push(new folder_1.default(this, folderElement.filename.replace(/\\/g, "/"), folderElement.basename, folderElement.lastmod, folderElement.fileid)); } return folders; }); } /** * get files of a folder * @param folderName Name of the folder like "/company/branches/germany" * @returns array of file objects */ getFiles(folderName) { return __awaiter(this, void 0, void 0, function* () { debug("getFiles: folder %s", folderName); const files = []; folderName = this.sanitizeFolderName(folderName); const fileElements = yield this.Contents(folderName, false); for (const folderElement of fileElements) { debug("getFiles: adding file %s", folderElement.filename); // debug("getFiles: adding file %O", folderElement); files.push(new file_1.default(this, folderElement.filename.replace(/\\/g, "/"), folderElement.basename, folderElement.lastmod, folderElement.size, folderElement.mime, folderElement.fileid)); } return files; }); } /** * create a new file of overwrites an existing file * @param fileName the file name /folder1/folder2/filename.txt * @param data the buffer object */ createFile(fileName, data) { return __awaiter(this, void 0, void 0, function* () { if (fileName.startsWith("./")) { fileName = fileName.replace("./", "/"); } const baseName = path_1.default.basename(fileName); const folderName = path_1.default.dirname(fileName); debug("createFile folder name %s base name %s", folderName, baseName); // ensure that we have a folder yield this.createFolder(folderName); yield this.putFileContents(fileName, data); let file; file = yield this.getFile(fileName); if (!file) { throw new error_1.default(`Error creating file, file name "${fileName}"`, "ERR_CREATE_FILE_FAILED"); } return file; }); } /** * returns a nextcloud file object * @param fileName the full file name /folder1/folder2/file.pdf */ getFile(fileName) { return __awaiter(this, void 0, void 0, function* () { debug("getFile fileName = %s", fileName); try { const stat = yield this.stat(fileName); debug(": SUCCESS!!"); if (stat.type === "file") { return new file_1.default(this, stat.filename.replace(/\\/g, "/"), stat.basename, stat.lastmod, stat.size, stat.mime || "", stat.fileid || -1); } else { debug("getFile: found object is a folder not a file"); return null; } } catch (e) { debug("getFile: exception occurred calling stat %O", e.message); return null; } }); } /** * renames the file or moves it to an other location * @param sourceFileName source file name * @param targetFileName target file name */ moveFile(sourceFileName, targetFileName) { return __awaiter(this, void 0, void 0, function* () { const url = this.webDAVUrl + sourceFileName; const destinationUrl = this.webDAVUrl + targetFileName; debug("moveFile from '%s' to '%s'", url, destinationUrl); const requestInit = { headers: new node_fetch_1.Headers({ Destination: destinationUrl }), method: "MOVE", }; try { yield this.getHttpResponse(url, requestInit, [201], { description: "File move" }); } catch (err) { debug("Error in move file %s %s source: %s destination: %s", err.message, requestInit.method, url, destinationUrl); throw new error_1.default("Error: moving file failed: source=" + sourceFileName + " target=" + targetFileName + " - " + err.message, "ERR_FILE_MOVE_FAILED"); } const targetFile = yield this.getFile(targetFileName); if (!targetFile) { throw new error_1.default("Error: moving file failed: source=" + sourceFileName + " target=" + targetFileName, "ERR_FILE_MOVE_FAILED"); } return targetFile; }); } /** * renames the folder or moves it to an other location * @param sourceFolderName source folder name * @param tarName target folder name */ moveFolder(sourceFolderName, tarName) { return __awaiter(this, void 0, void 0, function* () { const url = this.webDAVUrl + sourceFolderName; const destinationUrl = this.webDAVUrl + tarName; debug("moveFolder from '%s' to '%s'", url, destinationUrl); const requestInit = { headers: new node_fetch_1.Headers({ Destination: destinationUrl }), method: "MOVE", }; try { yield this.getHttpResponse(url, requestInit, [201], { description: "Folder move" }); } catch (err) { debug("Error in move folder %s %s source: %s destination: %s", err.message, requestInit.method, url, destinationUrl); throw new error_1.default("Error: moving folder failed: source=" + sourceFolderName + " target=" + tarName + " - " + err.message, "ERR_FOLDER_MOVE_FAILED"); } const tar = yield this.getFolder(tarName); if (!tar) { throw new error_1.default("Error: moving folder failed: source=" + sourceFolderName + " target=" + tarName, "ERR_FOLDER_MOVE_FAILED"); } return tar; }); } /** * returns the content of a file * @param fileName name of the file /d1/file1.txt * @returns Buffer with file content */ getContent(fileName) { return __awaiter(this, void 0, void 0, function* () { const url = this.webDAVUrl + fileName; debug("getContent GET %s", url); const requestInit = { method: "GET", }; let response; try { response = yield this.getHttpResponse(url, requestInit, [200], { description: "File get content" }); } catch (err) { debug("Error getContent %s - error %s", url, err.message); throw err; } return Buffer.from(yield response.buffer()); }); } /** * returns the link to a file for downloading * @param fileName name of the file /folder1/folder1.txt * @returns url */ getLink(fileName) { debug("getLink of %s", fileName); return this.webDAVUrl + fileName; } /** * returns the url to the file in the nextcloud UI * @param fileId the id of the file */ getUILink(fileId) { debug("getUILink of %s", fileId); return `${this.nextcloudOrigin}/apps/files/?fileid=${fileId}`; } /** * adds a tag to a file or folder * if the tag does not exist, it is automatically created * if the tag is created, the user must have damin privileges * @param fileId the id of the file * @param tagName the name of the tag * @returns nothing * @throws Error */ addTagToFile(fileId, tagName) { return __awaiter(this, void 0, void 0, function* () { debug("addTagToFile file:%s tag:%s", fileId, tagName); const tag = yield this.createTag(tagName); if (!tag.canAssign) { throw new error_1.default(`Error: No permission to assign tag "${tagName}" to file. Tag is not assignable`, "ERR_TAG_NOT_ASSIGNABLE"); } const addTagBody = { canAssign: tag.canAssign, id: tag.id, name: tag.name, userAssignable: tag.assignable, userVisible: tag.visible, }; const requestInit = { body: JSON.stringify(addTagBody, null, 4), headers: new node_fetch_1.Headers({ "Content-Type": "application/json" }), method: "PUT", }; yield this.getHttpResponse(`${this.nextcloudOrigin}/remote.php/dav/systemtags-relations/files/${fileId}/${tag.id}`, requestInit, [201, 409], { description: "File add tag" }); // created or conflict }); } // *************************************************************************************** // activity // *************************************************************************************** /* to be refactored to eventing public async getActivities(): Promise<string[]> { const result: string[] = []; const requestInit: RequestInit = { headers: new Headers({ "ocs-apirequest": "true" }), method: "GET", }; const response: Response = await this.getHttpResponse( this.nextcloudOrigin + "/ocs/v2.php/apps/activity/api/v2/activity/files?format=json&previews=false&since=97533", requestInit, [200], { description: "Activities get" }); const responseObject: any = await response.json(); // @todo for (const res of responseObject.ocs.data) { debug(JSON.stringify({ acivityId: res.activity_id, objects: res.objects, type: res.type, }, null, 4)); } // debug("getActivities: responseObject %s", JSON.stringify(responseObject, null, 4)); return result; } */ // *************************************************************************************** // comments // *************************************************************************************** /** * adds a comment to a file * @param fileId the id of the file * @param comment the comment to be added to the file */ addCommentToFile(fileId, comment) { return __awaiter(this, void 0, void 0, function* () { debug("addCommentToFile file:%s comment:%s", fileId, comment); const addCommentBody = { actorType: "users", message: comment, objectType: "files", verb: "comment", }; const requestInit = { body: JSON.stringify(addCommentBody, null, 4), headers: new node_fetch_1.Headers({ "Content-Type": "application/json" }), method: "POST", }; yield this.getHttpResponse(`${this.nextcloudOrigin}/remote.php/dav/comments/files/${fileId}`, requestInit, [201], { description: "File add comment" }); // created }); } /** * returns comments of a file / folder * @param fileId the id of the file / folder * @param top number of comments to return * @param skip the offset * @returns array of comment strings * @throws Exception */ getFileComments(fileId, top, skip) { return __awaiter(this, void 0, void 0, function* () { debug("getFileComments fileId:%s", fileId); if (!top) { top = 30; } if (!skip) { skip = 0; } const requestInit = { body: `<?xml version="1.0" encoding="utf-8" ?> <oc:filter-comments xmlns:D="DAV:" xmlns:oc="http://owncloud.org/ns"> <oc:limit>${top}</oc:limit> <oc:offset>${skip}</oc:offset> </oc:filter-comments>`, method: "REPORT", }; const response = yield this.getHttpResponse(`${this.nextcloudOrigin}/remote.php/dav/comments/files/${fileId}`, requestInit, [207], { description: "File get comments" }); const properties = yield this.getPropertiesFromWebDAVMultistatusResponse(response, ""); const comments = []; for (const prop of properties) { comments.push(prop.message); } return comments; }); } /** * returns system information about the nextcloud server and the nextcloud client */ getSystemInfo() { return __awaiter(this, void 0, void 0, function* () { const requestInit = { headers: new node_fetch_1.Headers({ "ocs-apirequest": "true" }), method: "GET", }; const response = yield this.getHttpResponse(this.nextcloudOrigin + "/ocs/v2.php/apps/serverinfo/api/v1/info?format=json", requestInit, [200], { description: "SystemInfo get" }); const rawResult = yield response.json(); // validate the raw result let version; if (rawResult.ocs && rawResult.ocs.data && rawResult.ocs.data.nextcloud && rawResult.ocs.data.nextcloud.system && rawResult.ocs.data.nextcloud.system.version) { version = rawResult.ocs.data.nextcloud.system.version; } else { throw new error_1.default("Fatal Error: nextcloud system version missing", "ERR_SYSTEM_INFO_MISSING_DATA"); } const result = { nextcloud: { system: { version, }, }, nextcloudClient: { version: require("../package.json").version, }, }; return result; }); } // *************************************************************************************** // user management // *************************************************************************************** /** * returns users */ getUserIDs() { return __awaiter(this, void 0, void 0, function* () { const requestInit = { headers: new node_fetch_1.Headers({ "OCS-APIRequest": "true", "Accept": "application/json" }), method: "GET", }; const response = yield this.getHttpResponse( // ?perPage=1 page= this.nextcloudOrigin + "/ocs/v1.php/cloud/users", requestInit, [200], { description: "Users get" }); const rawResult = yield response.json(); let users = []; if (rawResult.ocs && rawResult.ocs.data && rawResult.ocs.data.users) { users = rawResult.ocs.data.users; } return users; }); } createUser(options) { return __awaiter(this, void 0, void 0, function* () { const requestInit = { body: JSON.stringify(options, null, 4), headers: new node_fetch_1.Headers({ "Content-Type": "application/x-www-form-urlencoded", "OCS-APIRequest": "true", }), method: "POST", }; debug("request body: ", requestInit.body); const response = yield this.getHttpResponse(this.nextcloudOrigin + "/ocs/v1.php/cloud/users", requestInit, [200], { description: "User create" }); const rawResult = yield response.json(); debug(rawResult); }); } // *************************************************************************************** // shares // https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-share-api.html // *************************************************************************************** /** * create a new share */ createShare(options) { return __awaiter(this, void 0, void 0, function* () { const shareRequest = share_1.default.createShareRequestBody(options); debug(shareRequest); const headers = { "Accept": "application/json", "Content-Type": "application/json;charset=UTF-8", "OCS-APIRequest": "true", }; const requestInit = { body: shareRequest, headers: new node_fetch_1.Headers(headers), method: "POST", }; const url = this.nextcloudOrigin + "/ocs/v2.php/apps/files_sharing/api/v1/shares"; // try { const response = yield this.getHttpResponse(url, requestInit, [200], { description: "Share create" }); const rawResult = yield response.json(); debug(rawResult); return share_1.default.getShare(this, rawResult.ocs.data.id); /* } catch (e) { debug("result " + e.message); debug("requestInit ", JSON.stringify(requestInit, null, 4)); debug("headers " + JSON.stringify(headers, null, 4)); debug("url ", url); throw e; } */ }); } /** * update a new share */ updateShare(shareId, body) { return __awaiter(this, void 0, void 0, function* () { debug("updateShare body ", body); const headers = { "Accept": "application/json", "Content-Type": "application/json;charset=UTF-8", "OCS-APIRequest": "true", }; const requestInit = { body: JSON.stringify(body, null, 4), headers: new node_fetch_1.Headers(headers), method: "PUT", }; const url = this.nextcloudOrigin + "/ocs/v2.php/apps/files_sharing/api/v1/shares/" + shareId; yield this.getHttpResponse(url, requestInit, [200], { description: "Share update" }); }); } /** * get share information * @param shareId */ getShare(shareId) { return __awaiter(this, void 0, void 0, function* () { const headers = { "Accept": "application/json", "OCS-APIRequest": "true", }; const requestInit = { headers: new node_fetch_1.Headers(headers), method: "GET", }; const url = this.nextcloudOrigin + "/ocs/v2.php/apps/files_sharing/api/v1/shares/" + shareId; const response = yield this.getHttpResponse(url, requestInit, [200], { description: "Share get" }); const rawResult = yield response.json(); return rawResult; /* } catch (e) { debug("result " + e.message); debug("requestInit ", JSON.stringify(requestInit, null, 4)); debug("headers " + JSON.stringify(headers, null, 4)); debug("url ", url); throw e; } */ }); } /** * get share information * @param shareId */ deleteShare(shareId) { return __awaiter(this, void 0, void 0, function* () { const headers = { "Accept": "application/json", "OCS-APIRequest": "true", }; const requestInit = { headers: new node_fetch_1.Headers(headers), method: "DELETE", }; const url = this.nextcloudOrigin + "/ocs/v2.php/apps/files_sharing/api/v1/shares/" + shareId; const response = yield this.getHttpResponse(url, requestInit, [200], { description: "Share delete" }); }); } // *************************************************************************************** // private methods // *************************************************************************************** /** * asserts valid xml * asserts multistatus response * asserts that a href is available in the multistatus response * asserts propstats and prop * @param response the http response * @param href get only properties that match the href * @returns array of properties * @throws GeneralError */ getPropertiesFromWebDAVMultistatusResponse(response, href) { return __awaiter(this, void 0, void 0, function* () { const responseContentType = response.headers.get("Content-Type"); if (!responseContentType) { throw new error_1.default("Response content type expected", "ERR_RESPONSE_WITHOUT_CONTENT_TYPE_HEADER"); } if (responseContentType.indexOf("application/xml") === -1) { throw new error_1.default("XML response content type expected", "ERR_XML_RESPONSE_CONTENT_TYPE_EXPECTED"); } const xmlBody = yield response.text(); if (fast_xml_parser_1.default.validate(xmlBody) !== true) { throw new error_1.default(`The response is not valid XML: ${xmlBody}`, "ERR_RESPONSE_NOT_INVALID_XML"); } const options = { ignoreNameSpace: true, }; const body = fast_xml_parser_1.default.parse(xmlBody, options); // ensure that we have a multistatus response if (!body.multistatus || !body.multistatus.response) { throw new error_1.default(`The response is is not a WebDAV multistatus response`, "ERR_RESPONSE_NO_MULTISTATUS_XML"); } // ensure that response is always an array if (body.multistatus.response.href || body.multistatus.response.propstat) { body.multistatus.response = new Array(body.multistatus.response); } /* if (body.multistatus.response.propstat) { body.multistatus.response = [body.multistatus.response]; } */ const responseProperties = []; for (const res of body.multistatus.response) { if (!res.href) { throw new error_1.default(`The mulitstatus response must have a href`, "ERR_RESPONSE_MISSING_HREF_MULTISTATUS"); } if (!res.propstat) { throw new error_1.default(`The mulitstatus response must have a "propstat" container`, "ERR_RESPONSE_MISSING_PROPSTAT"); } let propStats = res.propstat; // ensure an array if (res.propstat.status || res.propstat.prop) { propStats = [res.propstat]; } for (const propStat of propStats) { if (!propStat.status) { throw new error_1.default(`The propstat must have a "status"`, "ERR_RESPONSE_MISSING_PROPSTAT_STATUS"); } if (propStat.status === "HTTP/1.1 200 OK") { if (!propStat.prop) { throw new error_1.default(`The propstat must have a "prop"`, "ERR_RESPONSE_MISSING_PROPSTAT_PROP"); } const property = propStat.prop; property._href = res.href; responseProperties.push(property); } } // } } return responseProperties; }); } /** * nextcloud creates a csrf token and stores it in the html header attribute * data-requesttoken * this function is currently not used * @returns the csrf token / requesttoken */ /* private async getCSRFToken(): Promise<string> { const requestInit: RequestInit = { method: "GET", }; const response: Response = await this.getHttpResponse( this.nextcloudOrigin, requestInit, [200], { description: "CSER token get" }); const html = await response.text(); const requestToken: string = html.substr(html.indexOf("data-requesttoken=") + 19, 89); debug("getCSRFToken %s", requestToken); return requestToken; } */ getHttpResponse(url, requestInit, expectedHttpStatusCode, context) { return __awaiter(this, void 0, void 0, function* () { if (!requestInit.headers) { requestInit.headers = new node_fetch_1.Headers(); } /* istanbul ignore else */ if (this.fakeServer) { return yield this.fakeServer.getFakeHttpResponse(url, requestInit, expectedHttpStatusCode, context); } else { return yield this.httpClient.getHttpResponse(url, requestInit, expectedHttpStatusCode, context); } }); } /** * get contents array of a folder * @param folderName Name of the folder like "/company/branches/germany" * @param folderIndicator true if folders are requested otherwise files * @returns array of folder contents meta data */ Contents(folderName, folderIndicator) { return __awaiter(this, void 0, void 0, function* () { debug("Contents: folder %s", folderName); const folders = []; folderName = this.sanitizeFolderName(folderName); const resultArray = []; if (folderIndicator === true) { debug("Contents: get folders"); } else { debug("Contents: get files"); } try { const folderContentsArray = yield this.getFolderContents(folderName); // debug("###########################"); // debug("$s", JSON.stringify(folderContentsArray, null, 4)); // debug("########################