nextcloud-node-client
Version:
Nextcloud client API for node.js applications
1,161 lines (1,151 loc) • 54.9 kB
JavaScript
"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("########################