From 94ec9ef22a3d8b1f13ac90864bbc7ce92ab754f6 Mon Sep 17 00:00:00 2001 From: Ellpeck Date: Mon, 3 Jul 2023 13:11:49 +0200 Subject: [PATCH] added the ability to add tags to files --- nextcloud-upload/README.md | 2 + nextcloud-upload/package-lock.json | 112 ++++++++++++++++++++++++++++- nextcloud-upload/package.json | 3 +- nextcloud-upload/run.js | 100 ++++++++++++++++++++++++-- nextcloud-upload/test.sh | 8 +++ 5 files changed, 217 insertions(+), 8 deletions(-) create mode 100644 nextcloud-upload/test.sh diff --git a/nextcloud-upload/README.md b/nextcloud-upload/README.md index 7e1db5a..a498508 100644 --- a/nextcloud-upload/README.md +++ b/nextcloud-upload/README.md @@ -16,4 +16,6 @@ steps: basedir: "." # optional, local base directory for files, defaults to . chunksize: # optional, chunk size in bytes, defaults to 10485760, or 10 MiB quiet: false # optional, whether to reduce output + tags: # optional, a set of tags to apply to uploaded files, tag is expected to already exist + - mytag ``` diff --git a/nextcloud-upload/package-lock.json b/nextcloud-upload/package-lock.json index 2ed54da..d14767e 100644 --- a/nextcloud-upload/package-lock.json +++ b/nextcloud-upload/package-lock.json @@ -7,10 +7,11 @@ "": { "name": "nextcloud-upload", "version": "1.0.0", - "license": "ISC", + "license": "MIT", "dependencies": { "glob": "^10.3.1", - "nextcloud-chunk-file-upload": "^1.1.1" + "nextcloud-chunk-file-upload": "^1.1.1", + "xml2json": "^0.12.0" } }, "node_modules/@isaacs/cliui": { @@ -73,6 +74,14 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -120,6 +129,11 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, "node_modules/follow-redirects": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", @@ -175,6 +189,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/hoek": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", + "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", + "deprecated": "This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -183,6 +206,17 @@ "node": ">=8" } }, + "node_modules/isemail": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz", + "integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==", + "dependencies": { + "punycode": "2.x.x" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -205,6 +239,29 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/joi": { + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-13.7.0.tgz", + "integrity": "sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q==", + "deprecated": "This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).", + "dependencies": { + "hoek": "5.x.x", + "isemail": "3.x.x", + "topo": "3.x.x" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/joi/node_modules/hoek": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", + "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==", + "deprecated": "This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).", + "engines": { + "node": ">=8.9.0" + } + }, "node_modules/lru-cache": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.0.tgz", @@ -235,6 +292,11 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/nan": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", + "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==" + }, "node_modules/nextcloud-chunk-file-upload": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/nextcloud-chunk-file-upload/-/nextcloud-chunk-file-upload-1.1.1.tgz", @@ -243,6 +305,16 @@ "axios": "^0.24.0" } }, + "node_modules/node-expat": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/node-expat/-/node-expat-2.4.0.tgz", + "integrity": "sha512-X8Y/Zk/izfNgfayeOeUGqze7KlaOwVJ9SDTjHUMKd0hu0aFTRpLlLCBwmx79cTPiQWD24I1YOafF+U+rTvEMfQ==", + "hasInstallScript": true, + "dependencies": { + "bindings": "^1.5.0", + "nan": "^2.13.2" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -266,6 +338,14 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "engines": { + "node": ">=6" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -384,6 +464,21 @@ "node": ">=8" } }, + "node_modules/topo": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/topo/-/topo-3.0.3.tgz", + "integrity": "sha512-IgpPtvD4kjrJ7CRA3ov2FhWQADwv+Tdqbsf1ZnPUSAtCJ9e1Z44MmoSGDXGk4IppoZA7jd/QRkNddlLJWlUZsQ==", + "deprecated": "This module has moved and is now available at @hapi/topo. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues.", + "dependencies": { + "hoek": "6.x.x" + } + }, + "node_modules/topo/node_modules/hoek": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.3.tgz", + "integrity": "sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ==", + "deprecated": "This module has moved and is now available at @hapi/hoek. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues." + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -481,6 +576,19 @@ "engines": { "node": ">=8" } + }, + "node_modules/xml2json": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/xml2json/-/xml2json-0.12.0.tgz", + "integrity": "sha512-EPJHRWJnJUYbJlzR4pBhZODwWdi2IaYGtDdteJi0JpZ4OD31IplWALuit8r73dJuM4iHZdDVKY1tLqY2UICejg==", + "dependencies": { + "hoek": "^4.2.1", + "joi": "^13.1.2", + "node-expat": "^2.3.18" + }, + "bin": { + "xml2json": "bin/xml2json" + } } } } diff --git a/nextcloud-upload/package.json b/nextcloud-upload/package.json index 20886e0..b4c36d0 100644 --- a/nextcloud-upload/package.json +++ b/nextcloud-upload/package.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "glob": "^10.3.1", - "nextcloud-chunk-file-upload": "^1.1.1" + "nextcloud-chunk-file-upload": "^1.1.1", + "xml2json": "^0.12.0" } } diff --git a/nextcloud-upload/run.js b/nextcloud-upload/run.js index 04ce639..6975343 100644 --- a/nextcloud-upload/run.js +++ b/nextcloud-upload/run.js @@ -1,6 +1,7 @@ const Upload = require("nextcloud-chunk-file-upload"); const glob = require("glob"); const axios = require("axios"); +const xml = require('xml2json'); // parse arguments const serverEnv = process.env.PLUGIN_SERVER; @@ -22,12 +23,52 @@ if (!destEnv) const baseDir = process.env.PLUGIN_BASEDIR || "."; const chunkSizeEnv = process.env.PLUGIN_CHUNKSIZE || 10 * 1024 * 1024; const quiet = process.env.PLUGIN_QUIET || false; +const tags = process.env.PLUGIN_TAGS || ""; upload(); async function upload() { let basePath = `${serverEnv}/remote.php/dav`; - const upload = new Upload(basePath, userEnv, userEnv, tokenEnv); + + // find ids for the tags we want to assign later + let tagIds = new Map(); + if (tags) { + if (!quiet) + console.log(`Retrieving tag ids`); + // list tags and find the ones we want: https://doc.owncloud.com/server/next/developer_manual/webdav_api/tags.html#list-tags + let response = await axios.request({ + method: "propfind", + url: `${basePath}/systemtags`, + auth: { + username: userEnv, + password: tokenEnv + }, + data: ` + + + + + + ` + }); + var data = JSON.parse(xml.toJson(response.data))["d:multistatus"]["d:response"].map(e => e["d:propstat"]["d:prop"]); + for (let tag of tags.split(",")) { + let entry = data.find(e => e["oc:display-name"] == tag); + if (!entry) { + console.log(`Couldn't find tag with name ${tag}`); + process.exit(1); + } + let tagId = entry["oc:id"]; + tagIds.set(tag, tagId); + if (!quiet) + console.log(`Tag id of tag ${tag} is ${tagId}`); + } + } + + // collect and upload files + if (!quiet) + console.log(`Uploading files`); + let upload = new Upload(basePath, userEnv, userEnv, tokenEnv); for (let pattern of filesEnv.split(",")) { let files = await glob.glob(pattern, { cwd: baseDir }); if (!files.length) @@ -49,23 +90,72 @@ async function upload() { auth: { username: userEnv, password: tokenEnv - } + }, + // 405 means the directory already exists + validateStatus: s => s == 201 || s == 405 }); if (!quiet) console.log(`Created directory ${currDir}`); } catch (error) { - // this is fine since the directory likely already exists + console.log(`Failed to create directory ${currDir} (${error})`); + process.exit(1); } } // use lib to upload file await upload.uploadFile(`${baseDir}/${file}`, dest, parseInt(chunkSizeEnv)).then(e => { if (!quiet) - console.log(`Uploaded ${file} to ${dest} (${e})`); + console.log(`Uploaded ${file} to ${dest}`); }).catch(e => { - console.log(`Failed to upload file ${file} to ${dest} (${e}, error ${e.httpErrorCode}, ${e.httpErrorMessage})`); + console.log(`Failed to upload file ${file} to ${dest} (${e})`); process.exit(1); }); + + // add tags + if (tagIds.size) { + try { + // get file id: https://docs.nextcloud.com/server/latest/developer_manual/client_apis/WebDAV/basic.html#requesting-properties + let response = await axios.request({ + method: "propfind", + url: `${basePath}/files/${userEnv}/${dest}`, + auth: { + username: userEnv, + password: tokenEnv + }, + data: ` + + + + + ` + }); + var data = JSON.parse(xml.toJson(response.data)); + let fileId = data["d:multistatus"]["d:response"]["d:propstat"]["d:prop"]["oc:fileid"]; + if (!quiet) + console.log(`File id of file ${file} is ${fileId}`); + + // add tags: https://doc.owncloud.com/server/next/developer_manual/webdav_api/tags.html#assign-a-tag-to-a-file + for (let [tag, tagId] of tagIds.entries()) { + await axios.request({ + method: "put", + url: `${basePath}/systemtags-relations/files/${fileId}/${tagId}`, + auth: { + username: userEnv, + password: tokenEnv + }, + // 409 conflicted means the tag is already applied + validateStatus: s => s == 201 || s == 409 + }); + if (!quiet) + console.log(`Added tag ${tag} to file ${file}`); + } + + } catch (error) { + console.log(`Failed to assign tags ${tags} to file ${file} (${error})`); + process.exit(1); + } + + } } } } diff --git a/nextcloud-upload/test.sh b/nextcloud-upload/test.sh new file mode 100644 index 0000000..5e7ce68 --- /dev/null +++ b/nextcloud-upload/test.sh @@ -0,0 +1,8 @@ +export PLUGIN_SERVER=https://cloud.ellpeck.de +export PLUGIN_USER=EllBot +export PLUGIN_FILES=Dockerfile +export PLUGIN_DEST=Test/Testing +export PLUGIN_TAGS=testing,another + +npm install +node run.js