├── compiled ├── tests │ ├── webdav-jest.d.ts │ ├── prepare-docker-nextcloud-container.d.ts │ └── configuration.d.ts ├── source │ ├── common.d.ts │ ├── errors.d.ts │ ├── ocs │ │ ├── activity.d.ts │ │ ├── helper.d.ts │ │ ├── ocs-connection.d.ts │ │ ├── group.d.ts │ │ ├── share.d.ts │ │ ├── user.d.ts │ │ ├── groupfolders.d.ts │ │ ├── types.d.ts │ │ └── ocs.d.ts │ ├── helper.d.ts │ ├── webdav.d.ts │ ├── client.d.ts │ └── types.d.ts └── helper.js ├── .gitignore ├── cloud.png ├── .github ├── FUNDING.yml └── workflows │ └── nodejs.yml ├── .editorconfig ├── .npmignore ├── jest.conf.js ├── webpack.config.js ├── tsconfig.json ├── tests ├── configuration.ts ├── prepare-docker-nextcloud-container.ts ├── groupfolders-jest.ts └── webdav-jest.ts ├── LICENSE ├── source ├── errors.ts ├── common.ts ├── helper.ts ├── ocs │ ├── activity.ts │ ├── helper.ts │ ├── ocs-connection.ts │ ├── types.ts │ ├── group.ts │ ├── share.ts │ ├── user.ts │ ├── groupfolders.ts │ └── ocs.ts ├── types.ts ├── client.ts └── webdav.ts ├── docker-compose.yml ├── package.json ├── tslint.json └── README.md /compiled/tests/webdav-jest.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | local.json 3 | .DS_store -------------------------------------------------------------------------------- /compiled/tests/prepare-docker-nextcloud-container.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tentwentyfour/nextcloud-link/HEAD/cloud.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [tentwentyfour] 4 | -------------------------------------------------------------------------------- /compiled/tests/configuration.d.ts: -------------------------------------------------------------------------------- 1 | declare let configuration: any; 2 | export default configuration; 3 | -------------------------------------------------------------------------------- /compiled/source/common.d.ts: -------------------------------------------------------------------------------- 1 | export declare function getCreatorByPath(path: string): Promise; 2 | export declare function getCreatorByFileId(fileId: number | string): Promise; 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | cloud.png 2 | source/ 3 | tests/ 4 | compiled/tests 5 | .github/ 6 | .editorconfig 7 | docker-compose.yml 8 | jest.conf.js 9 | tsconfig.json 10 | tslint.json 11 | webpack.config.js 12 | -------------------------------------------------------------------------------- /jest.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ["ts", "js"], 3 | testMatch: ["**/*-jest\.ts"], 4 | transform: { "\\.ts$": "ts-jest" }, 5 | testRunner: "jest-jasmine2", 6 | }; 7 | -------------------------------------------------------------------------------- /compiled/source/errors.d.ts: -------------------------------------------------------------------------------- 1 | export declare const Exception: any; 2 | export declare const ForbiddenError: any; 3 | export declare const NotFoundError: any; 4 | export declare const NotReadyError: any; 5 | export declare const OcsError: any; 6 | -------------------------------------------------------------------------------- /compiled/source/ocs/activity.d.ts: -------------------------------------------------------------------------------- 1 | import { OcsActivity } from './types'; 2 | export declare function ocsGetActivities(fileId: number, sort: 'asc' | 'desc', limit: number, sinceActivityId: number, callback: (error: { 3 | code: any; 4 | message: any; 5 | }, activities?: OcsActivity[]) => void): void; 6 | -------------------------------------------------------------------------------- /compiled/source/ocs/helper.d.ts: -------------------------------------------------------------------------------- 1 | import { OcsSharePermissions } from './types'; 2 | export interface ErrorInfo { 3 | expectedErrorCodes?: number[]; 4 | customErrors?: { 5 | [key: number]: string; 6 | }; 7 | identifier?: string | number; 8 | message: string; 9 | useMeta: boolean; 10 | } 11 | export declare function rejectWithOcsError(error: any, errorInfo: ErrorInfo): Promise; 12 | export declare function assignDefined(target: any, ...sources: any[]): void; 13 | export declare function ocsSharePermissionsToText(permissions: OcsSharePermissions): string; 14 | -------------------------------------------------------------------------------- /compiled/source/helper.d.ts: -------------------------------------------------------------------------------- 1 | import { FileDetailProperty, AsyncFunction } from './types'; 2 | export declare function createFileDetailProperty(namespace: string, namespaceShort: string, element: string, nativeType?: boolean, defaultValue?: any): FileDetailProperty; 3 | export declare function createOwnCloudFileDetailProperty(element: string, nativeType?: boolean, defaultValue?: any): FileDetailProperty; 4 | export declare function createNextCloudFileDetailProperty(element: string, nativeType?: boolean, defaultValue?: any): FileDetailProperty; 5 | export declare function clientFunction(λ: T): T; 6 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | target: "node", 5 | mode: "production", 6 | resolve: { extensions: [".ts", ".js"] }, 7 | entry: { 8 | client: "./source/client.ts", 9 | helper: "./source/helper.ts" 10 | }, 11 | 12 | output: { 13 | path: path.resolve("./compiled"), 14 | library: "", 15 | filename: "[name].js", 16 | libraryTarget: "commonjs2" 17 | }, 18 | 19 | module: { 20 | rules: [{ 21 | test: /\.ts$/, 22 | loader: "ts-loader", 23 | exclude: /node_modules/ 24 | }] 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /compiled/source/ocs/ocs-connection.d.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionOptions } from '../types'; 2 | import { OcsHttpError } from './types'; 3 | export declare class OcsConnection { 4 | options: ConnectionOptions; 5 | constructor(url: string); 6 | constructor(options: ConnectionOptions); 7 | getHeader(withBody?: boolean): { 8 | 'Content-Type': string; 9 | 'OCS-APIRequest': boolean; 10 | Accept: string; 11 | Authorization: string; 12 | }; 13 | isValidResponse(body: any): boolean; 14 | request(error: any, response: any, body: any, callback: (error: OcsHttpError, body?: any) => any): void; 15 | } 16 | export default OcsConnection; 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["source/**/*.ts", "tests/**/*.ts", "node_modules/@types"], 3 | "atom": { "rewriteTsconfig": false }, 4 | 5 | "compilerOptions": { 6 | "baseUrl": "./", 7 | "outDir": "./compiled/", 8 | "declaration": true, 9 | "experimentalDecorators": true, 10 | "sourceMap": true, 11 | "strict": false, 12 | "target": "es2018", 13 | "esModuleInterop": true, 14 | "moduleResolution": "node", 15 | "types": ["node"], 16 | "module": "commonjs", 17 | "lib": ["es2018"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /compiled/source/ocs/group.d.ts: -------------------------------------------------------------------------------- 1 | import { OcsHttpError } from './types'; 2 | export declare function ocsListGroups(search: string, limit: number, offset: number, callback: (error: OcsHttpError, result?: string[]) => void): void; 3 | export declare function ocsAddGroup(groupId: string, callback: (error: OcsHttpError, result?: boolean) => void): void; 4 | export declare function ocsDeleteGroup(groupId: string, callback: (error: OcsHttpError, result?: boolean) => void): void; 5 | export declare function ocsGetGroupUsers(groupId: string, callback: (error: OcsHttpError, result?: string[]) => void): void; 6 | export declare function ocsGetGroupSubAdmins(groupId: string, callback: (error: OcsHttpError, result?: string[]) => void): void; 7 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | push: 8 | branches: 9 | - master 10 | - release/* 11 | - greenkeeper/* 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | node-version: [10.x, 12.x, 14.x] 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - run: npm install 26 | - run: npm run build --if-present 27 | - run: npm run test-ci 28 | env: 29 | CI: true 30 | - run: npm run docker:stop 31 | -------------------------------------------------------------------------------- /compiled/source/ocs/share.d.ts: -------------------------------------------------------------------------------- 1 | import { OcsSharePermissions, OcsEditShareField, OcsHttpError, OcsShareType, OcsShare } from './types'; 2 | export declare function ocsGetShares(path: string, includeReshares: boolean, showForSubFiles: boolean, callback: (error: OcsHttpError, result?: OcsShare[]) => void): void; 3 | export declare function ocsGetShare(shareId: number | string, callback: (error: OcsHttpError, result?: OcsShare) => void): void; 4 | export declare function ocsDeleteShare(shareId: number | string, callback: (error: OcsHttpError, result?: boolean) => void): void; 5 | export declare function ocsAddShare(path: string, shareType: OcsShareType, shareWith: string, permissions: OcsSharePermissions, password: string, publicUpload: boolean, callback: (error: OcsHttpError, result?: OcsShare) => void): void; 6 | export declare function ocsEditShare(shareId: number | string, field: OcsEditShareField, value: string, callback: (error: OcsHttpError, result?: OcsShare) => void): void; 7 | -------------------------------------------------------------------------------- /tests/configuration.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionOptions } from '../source/types'; 2 | import { join } from 'path'; 3 | import * as fs from 'fs'; 4 | 5 | const dockerConfig = fs.readFileSync(join(__dirname, '../docker-compose.yml')).toString(); 6 | const overrideFile = join(__dirname, '../local.json'); 7 | const overrideConfig = (fs.existsSync(overrideFile) ? JSON.parse(fs.readFileSync(overrideFile).toString()) : {}); 8 | 9 | const baseConfig = { 10 | address: 'localhost', 11 | port: dockerConfig.match(/(\d+):80/)[1], 12 | username: dockerConfig.match(/NEXTCLOUD_ADMIN_USER=(.*)/)[1], 13 | password: dockerConfig.match(/NEXTCLOUD_ADMIN_PASSWORD=(.*)/)[1], 14 | connectionOptions: null as ConnectionOptions 15 | }; 16 | 17 | let configuration = Object.assign({}, baseConfig, overrideConfig); 18 | configuration.connectionOptions = { 19 | username: configuration.username, 20 | password: configuration.password, 21 | url: `http://${configuration.address}:${configuration.port}` 22 | } as ConnectionOptions; 23 | 24 | export default configuration; 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 tentwentyfour 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /source/errors.ts: -------------------------------------------------------------------------------- 1 | import createErrorType from 'helpbox/source/create-error-type'; 2 | 3 | export const Exception = createErrorType(); 4 | 5 | export const ForbiddenError = createErrorType( 6 | function forbiddenErrorConstructor(error, path) { 7 | error.message = `Access to ${path} was denied`; 8 | }, 9 | 10 | Exception 11 | ); 12 | 13 | export const NotFoundError = createErrorType( 14 | function notFoundErrorConstructor(error, path) { 15 | error.message = `${path} not found!`; 16 | }, 17 | 18 | Exception 19 | ); 20 | 21 | export const NotReadyError = createErrorType( 22 | function notReadyErrorConstructor(error) { 23 | error.message = 'The Nextcloud instance is initializing…'; 24 | }, 25 | 26 | Exception 27 | ); 28 | 29 | export const OcsError = createErrorType( 30 | function ocsErrorConstructor(error, { message, identifier, reason, statusCode }) { 31 | const id = (identifier ? ` '${identifier}'` : ''); 32 | error.name = 'OcsError'; 33 | error.message = `${message}${id}: ${reason}`; 34 | if (statusCode) { 35 | error.statusCode = statusCode; 36 | } 37 | }, 38 | 39 | Exception 40 | ); 41 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | services: 3 | nextcloud: 4 | image: nextcloud:20-apache 5 | volumes: 6 | - nextcloud20Store:/var/www/html 7 | ports: 8 | - "16000:80" 9 | environment: 10 | - NEXTCLOUD_ADMIN_PASSWORD=nextcloud 11 | - NEXTCLOUD_ADMIN_USER=nextcloud 12 | - NEXTCLOUD_TRUSTED_DOMAINS=*.*.*.* 13 | - POSTGRES_DB=nextcloud 14 | - POSTGRES_PASSWORD=nextcloud 15 | - POSTGRES_USER=nextcloud 16 | - POSTGRES_HOST=database 17 | - SMTP_HOST=maildev 18 | - SMTP_PORT=25 19 | - SMTP_AUTHTYPE=NONE 20 | - MAIL_FROM_ADDRESS=nextcloud 21 | - MAIL_DOMAIN=1024.lu 22 | depends_on: 23 | - database 24 | - maildev 25 | database: 26 | image: postgres:11-alpine 27 | restart: always 28 | volumes: 29 | - database20Store:/var/lib/postgresql/data 30 | environment: 31 | - POSTGRES_DB=nextcloud 32 | - POSTGRES_USER=nextcloud 33 | - POSTGRES_PASSWORD=nextcloud 34 | maildev: 35 | image: djfarrelly/maildev 36 | ports: 37 | - "7078:80" 38 | - "7025:25" 39 | volumes: 40 | nextcloud20Store: 41 | database20Store: 42 | -------------------------------------------------------------------------------- /tests/prepare-docker-nextcloud-container.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * The docker container is weird. Accessing it the first time seems 3 | * to always return a 405 for some reason. Accessing it in a prepare 4 | * script seems to make it work just fine. 5 | */ 6 | 7 | import NextcloudClient from '../source/client'; 8 | import configuration from './configuration'; 9 | 10 | (async () => { 11 | const client = new NextcloudClient(configuration.connectionOptions); 12 | 13 | console.log('Waiting about 25 seconds for Nextcloud installation to complete. This can take a while, depending on your system…'); 14 | await new Promise(resolve => setTimeout(resolve, 25000)); 15 | console.info('Set-up should be complete by now. Starting to probe for readyness…'); 16 | let times = 0; 17 | 18 | while (true) { 19 | console.log('Checking nextcloud availability…'); 20 | 21 | if (await client.checkConnectivity()) { 22 | break; 23 | } 24 | 25 | times += 1; 26 | 27 | if (times > 10) { 28 | console.log('The nextcloud container does not seem to work. Aborting…'); 29 | process.exit(1); 30 | } 31 | 32 | await new Promise(resolve => setTimeout(resolve, 5000)); 33 | } 34 | 35 | process.exit(0); 36 | })(); 37 | -------------------------------------------------------------------------------- /source/common.ts: -------------------------------------------------------------------------------- 1 | import { createOwnCloudFileDetailProperty } from './helper'; 2 | import { NextcloudClientInterface } from './types'; 3 | 4 | export async function getCreatorByPath(path: string) : Promise { 5 | const self: NextcloudClientInterface = this; 6 | 7 | let result = null; 8 | 9 | try { 10 | const fileIdProp = createOwnCloudFileDetailProperty('fileid', true); 11 | const propName = `${fileIdProp.namespaceShort}:${fileIdProp.element}`; 12 | 13 | const folderProperties = await self.getFolderProperties(path, [fileIdProp]); 14 | 15 | const fileId = folderProperties[propName].content as string; 16 | result = await self.getCreatorByFileId(fileId); 17 | } catch { 18 | result = Promise.reject(new Error(`Unable to find the creator for '${path}'`)); 19 | } 20 | 21 | return result; 22 | } 23 | 24 | export async function getCreatorByFileId(fileId: number | string) : Promise { 25 | const self: NextcloudClientInterface = this; 26 | 27 | let result = null; 28 | 29 | try { 30 | const activities = await self.activities.get(fileId, 'asc', 1); 31 | const fileCreatedActivity = activities 32 | .find(activity => activity.type === 'file_created'); 33 | 34 | result = fileCreatedActivity.user; 35 | } catch { 36 | result = Promise.reject(new Error(`Unable to find the creator for fileId '${fileId}'`)); 37 | } 38 | 39 | return result; 40 | } 41 | -------------------------------------------------------------------------------- /compiled/source/ocs/user.d.ts: -------------------------------------------------------------------------------- 1 | import { OcsEditUserField, OcsHttpError, OcsNewUser, OcsUser } from './types'; 2 | export declare function ocsGetUser(userId: string, callback: (error: OcsHttpError, result?: OcsUser) => void): void; 3 | export declare function ocsListUsers(search: string, limit: number, offset: number, callback: (error: OcsHttpError, result?: string[]) => void): void; 4 | export declare function ocsSetUserEnabled(userId: string, isEnabled: boolean, callback: (error: OcsHttpError, result?: boolean) => void): void; 5 | export declare function ocsDeleteUser(userId: string, callback: (error: OcsHttpError, result?: boolean) => void): void; 6 | export declare function ocsAddUser(user: OcsNewUser, callback: (error: OcsHttpError, result?: boolean) => void): void; 7 | export declare function ocsEditUser(userId: string, field: OcsEditUserField, value: string, callback: (error: OcsHttpError, result?: boolean) => void): void; 8 | export declare function ocsGetUserGroups(userId: string, callback: (error: OcsHttpError, result?: string[]) => void): void; 9 | export declare function ocsAddRemoveUserForGroup(userId: string, groupId: string, toAdd: boolean, callback: (error: OcsHttpError, result?: boolean) => void): void; 10 | export declare function ocsSetUserSubAdmin(userId: string, groupId: string, isSubAdmin: boolean, callback: (error: OcsHttpError, result?: boolean) => void): void; 11 | export declare function ocsGetUserSubAdmins(userId: string, callback: (error: OcsHttpError, result?: string[]) => void): void; 12 | export declare function ocsResendUserWelcomeEmail(userId: string, callback: (error: OcsHttpError, result?: boolean) => void): void; 13 | -------------------------------------------------------------------------------- /compiled/source/ocs/groupfolders.d.ts: -------------------------------------------------------------------------------- 1 | import { OcsHttpError, OcsGroupfolder } from './types'; 2 | export declare function ocsGetGroupfolders(callback: (error: OcsHttpError, result?: OcsGroupfolder[]) => void): void; 3 | export declare function ocsGetGroupfolder(groupfolderId: number, callback: (error: OcsHttpError, result?: OcsGroupfolder) => void): void; 4 | export declare function ocsAddGroupfolder(mountpoint: string, callback: (error: OcsHttpError, result?: number) => void): void; 5 | export declare function ocsRemoveGroupfolder(groupfolderId: number, callback: (error: OcsHttpError, result?: boolean) => void): void; 6 | export declare function ocsAddGroupfolderGroup(groupfolderId: number, groupId: string, callback: (error: OcsHttpError, result?: boolean) => void): void; 7 | export declare function ocsRemoveGroupfolderGroup(groupfolderId: number, groupId: string, callback: (error: OcsHttpError, result?: boolean) => void): void; 8 | export declare function ocsSetGroupfolderPermissions(groupfolderId: number, groupId: string, permissions: number, callback: (error: OcsHttpError, result?: boolean) => void): void; 9 | export declare function ocsEnableOrDisableGroupfolderACL(groupfolderId: number, enable: boolean, callback: (error: OcsHttpError, result?: boolean) => void): void; 10 | export declare function ocsSetGroupfolderManageACL(groupfolderId: number, type: 'group' | 'user', id: string, manageACL: boolean, callback: (error: OcsHttpError, result?: boolean) => void): void; 11 | export declare function ocsSetGroupfolderQuota(groupfolderId: number, quota: number, callback: (error: OcsHttpError, result?: boolean) => void): void; 12 | export declare function ocsRenameGroupfolder(groupfolderId: number, mountpoint: string, callback: (error: OcsHttpError, result?: boolean) => void): void; 13 | -------------------------------------------------------------------------------- /source/helper.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FileDetailProperty, 3 | AsyncFunction, 4 | } from './types'; 5 | 6 | import { 7 | ForbiddenError, 8 | NotFoundError, 9 | } from './errors'; 10 | 11 | const sanitizePath = encodeURI; 12 | 13 | export function createFileDetailProperty( 14 | namespace: string, 15 | namespaceShort: string, 16 | element: string, 17 | nativeType?: boolean, 18 | defaultValue?: any 19 | ) : FileDetailProperty { 20 | return { 21 | namespace, 22 | namespaceShort, 23 | element, 24 | nativeType, 25 | default: defaultValue 26 | }; 27 | } 28 | 29 | export function createOwnCloudFileDetailProperty(element: string, nativeType?: boolean, defaultValue?: any) : FileDetailProperty { 30 | return createFileDetailProperty('http://owncloud.org/ns', 'oc', element, nativeType, defaultValue); 31 | } 32 | 33 | export function createNextCloudFileDetailProperty(element: string, nativeType?: boolean, defaultValue?: any) : FileDetailProperty { 34 | return createFileDetailProperty('http://nextcloud.org/ns', 'nc', element, nativeType, defaultValue); 35 | } 36 | 37 | export function clientFunction(λ: T): T { 38 | return async function errorTranslator(...parameters) { 39 | // This assumes the first parameter will always be the path. 40 | const path = parameters[0]; 41 | 42 | try { 43 | return await λ.apply(this, [sanitizePath(path)].concat(parameters.slice(1))); 44 | } catch (error) { 45 | let thrownError = error; 46 | 47 | if (error.statusCode) { 48 | if (error.statusCode === 404) { 49 | thrownError = new NotFoundError(path); 50 | } else if (error.statusCode === 403) { 51 | thrownError = new ForbiddenError(path); 52 | } 53 | } 54 | 55 | throw thrownError; 56 | } 57 | } as T; 58 | } 59 | -------------------------------------------------------------------------------- /source/ocs/activity.ts: -------------------------------------------------------------------------------- 1 | import * as querystring from 'querystring'; 2 | import req from 'request'; 3 | 4 | import { OcsConnection } from './ocs-connection'; 5 | import { OcsActivity } from './types'; 6 | 7 | const baseUrl = 'ocs/v2.php/apps/activity/api/v2/activity'; 8 | 9 | export function ocsGetActivities( 10 | fileId: number, 11 | sort: 'asc' | 'desc', 12 | limit: number, 13 | sinceActivityId: number, 14 | callback: (error: { code, message }, activities?: OcsActivity[]) => void 15 | ) : void { 16 | const self: OcsConnection = this; 17 | 18 | const params = { 19 | format: 'json', 20 | object_type: 'files', 21 | object_id: fileId, 22 | sort: (sort === 'asc' ? 'asc' : 'desc') 23 | }; 24 | 25 | if (limit > 0) { 26 | params['limit'] = limit; 27 | } 28 | 29 | if (sinceActivityId > 0) { 30 | params['since'] = sinceActivityId; 31 | } 32 | 33 | const urlParams = querystring.stringify(params); 34 | 35 | req({ 36 | url: `${self.options.url}/${baseUrl}/filter?${urlParams}`, 37 | headers: self.getHeader() 38 | }, (error, response, body) => { 39 | self.request(error, response, body, (error: { code, message }, body?) => { 40 | let activities: OcsActivity[] = []; 41 | 42 | if (!error && body && body.data && body.data.length > 0) { 43 | body.data.forEach(activity => { 44 | activities.push({ 45 | activityId: parseInt(activity.activity_id, 10), 46 | app: activity.app, 47 | type: activity.type, 48 | user: activity.user, 49 | subject: activity.subject, 50 | subjectRich: activity.subject_rich, 51 | message: activity.message, 52 | messageRich: activity.message_rich, 53 | objectType: activity.object_type, 54 | fileId: activity.objectId, 55 | objectName: activity.object_name, 56 | objects: activity.objects, 57 | link: activity.link, 58 | icon: activity.icon, 59 | datetime: activity.datetime 60 | }); 61 | }); 62 | } 63 | 64 | callback(error, activities); 65 | }); 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /source/ocs/helper.ts: -------------------------------------------------------------------------------- 1 | import { OcsSharePermissions } from './types'; 2 | import { OcsError } from '../errors'; 3 | 4 | export interface ErrorInfo { 5 | expectedErrorCodes?: number[]; 6 | customErrors?: { [key: number]: string }; 7 | identifier?: string | number; 8 | message: string; 9 | useMeta: boolean; 10 | } 11 | 12 | export function rejectWithOcsError( 13 | error, 14 | errorInfo: ErrorInfo 15 | ) : Promise { 16 | let reason = error.message; 17 | let statusCode = ''; 18 | if (( 19 | errorInfo.expectedErrorCodes === undefined || 20 | errorInfo.expectedErrorCodes.includes(error.code) 21 | ) && ( 22 | (errorInfo.useMeta && error.meta && error.meta.statuscode) || 23 | !errorInfo.useMeta 24 | )) { 25 | statusCode = (errorInfo.useMeta ? error.meta.statuscode : error.code); 26 | reason = (errorInfo.useMeta ? error.meta.message : reason); 27 | 28 | if (errorInfo.customErrors && errorInfo.customErrors.hasOwnProperty(statusCode)) { 29 | reason = errorInfo.customErrors[statusCode]; 30 | } 31 | } 32 | 33 | return Promise.reject(new OcsError({ 34 | reason, 35 | statusCode, 36 | message: errorInfo.message, 37 | identifier: errorInfo.identifier 38 | })); 39 | } 40 | 41 | export function assignDefined(target, ...sources) { 42 | for (const source of sources) { 43 | for (const key of Object.keys(source)) { 44 | const val = source[key]; 45 | if (val !== undefined) { 46 | target[key] = val; 47 | } 48 | } 49 | } 50 | } 51 | 52 | export function ocsSharePermissionsToText(permissions: OcsSharePermissions) : string { 53 | if (permissions === OcsSharePermissions.default) { 54 | return ''; 55 | } 56 | if (permissions === OcsSharePermissions.all) { 57 | return 'all'; 58 | } 59 | 60 | const result = []; 61 | Object.keys(OcsSharePermissions).forEach(key => { 62 | if (OcsSharePermissions[key] !== OcsSharePermissions.default && OcsSharePermissions[key] !== OcsSharePermissions.all) { 63 | if ((permissions & OcsSharePermissions[key]) === OcsSharePermissions[key]) { 64 | result.push(key); 65 | } 66 | } 67 | }); 68 | 69 | return result.join('|'); 70 | } 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextcloud-link", 3 | "version": "1.2.9", 4 | "description": "Javascript API to talk to Nextcloud", 5 | "main": "compiled/client.js", 6 | "scripts": { 7 | "build": "npm run compile && npm run types:generate", 8 | "compile": "npx esbuild --bundle --platform=node --target=es2018 source/client.ts --outdir=compiled --minify", 9 | "types:generate": "npx typescript source/*.ts source/**/*.ts --declaration --emitDeclarationOnly --outDir compiled/source/", 10 | "docker": "npm run docker:stop && docker-compose -p nextcloud-link up -d", 11 | "docker:stop": "docker-compose -p nextcloud-link down -v", 12 | "lint": "tslint --project tsconfig.json", 13 | "test": "npm run docker && ts-node tests/prepare-docker-nextcloud-container.ts && jest --config jest.conf.js --runInBand; npm run docker:stop", 14 | "test-ci": "npm run docker && ts-node tests/prepare-docker-nextcloud-container.ts && jest --config jest.conf.js --runInBand", 15 | "test-watch": "npm run docker && ts-node tests/prepare-docker-nextcloud-container.ts && jest --config jest.conf.js --runInBand --watch", 16 | "fast-test-watch": "ts-node tests/prepare-docker-nextcloud-container.ts && jest --config jest.conf.js --runInBand --watch", 17 | "fast-test-watch-nostart": "ts-node tests/prepare-docker-nextcloud-container.ts && jest --config jest.conf.js --runInBand --watch --testNamePattern=stopTestingAtStart" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/tentwentyfour/nextcloud-link.git" 22 | }, 23 | "keywords": [ 24 | "nextcloud" 25 | ], 26 | "author": "TenTwentyFour s.à r.l.", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/tentwentyfour/nextcloud-link/issues" 30 | }, 31 | "dependencies": { 32 | "helpbox": "^7.2.0", 33 | "webdav-client": "^1.4.3" 34 | }, 35 | "types": "./compiled/source/client.d.ts", 36 | "devDependencies": { 37 | "@types/jest": "^27.0.2", 38 | "@types/node": "^14.14.14", 39 | "esbuild": "^0.13.12", 40 | "jest": "^27.3.1", 41 | "node-notifier": ">=8.0.1", 42 | "ts-jest": "^27.0.7", 43 | "ts-loader": "^9.4.2", 44 | "ts-node": "^8.10.2", 45 | "tslint": "^6.1.3", 46 | "tslint-config-airbnb": "^5.11.1", 47 | "tslint-eslint-rules": "^5.4.0", 48 | "typescript": "^3.9.7" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /source/ocs/ocs-connection.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionOptions } from '../types'; 2 | import { OcsHttpError } from './types'; 3 | 4 | export class OcsConnection { 5 | options : ConnectionOptions; 6 | 7 | constructor(url: string) 8 | constructor(options : ConnectionOptions) 9 | constructor(options : string | ConnectionOptions) { 10 | if (options.constructor === String) { 11 | // tslint:disable-next-line: no-parameter-reassignment 12 | options = { url: options as string }; 13 | } 14 | this.options = options as ConnectionOptions; 15 | 16 | if (this.options.url.lastIndexOf('/') === this.options.url.length - 1) { 17 | this.options.url = this.options.url.substring(0, this.options.url.length - 1); 18 | } 19 | } 20 | 21 | getHeader(withBody?: boolean) { 22 | const credentials = Buffer.from(`${this.options.username}:${(this.options.password ? this.options.password : '')}`).toString('base64'); 23 | const header = { 24 | 'Content-Type': (withBody ? 'application/json' : 'application/x-www-form-urlencoded'), 25 | 'OCS-APIRequest' : true, 26 | Accept: 'application/json', 27 | Authorization: `Basic ${credentials}` 28 | }; 29 | 30 | return header; 31 | } 32 | 33 | isValidResponse(body) : boolean { 34 | return (body && body.ocs && body.ocs.meta); 35 | } 36 | 37 | request(error, response, body, callback: (error: OcsHttpError, body?: any) => any) { 38 | if (error) { 39 | callback(error, null); 40 | return; 41 | } 42 | 43 | let jsonBody; 44 | 45 | try { 46 | jsonBody = JSON.parse(body || '{}'); 47 | } catch { 48 | callback({ 49 | code: 500, 50 | message: 'Unable to parse the response body as valid JSON.' 51 | }); 52 | } 53 | 54 | if (response.statusCode !== 200) { 55 | callback({ 56 | code: response.statusCode, 57 | message: response.statusMessage, 58 | meta: (this.isValidResponse(jsonBody) ? jsonBody.ocs.meta : null) 59 | }, null); 60 | 61 | return; 62 | } 63 | 64 | if (this.isValidResponse(jsonBody)) { 65 | // Response is well-formed 66 | callback(null, jsonBody.ocs); 67 | } else { 68 | // Server said everything's fine but response is malformed 69 | callback({ 70 | code: 500, 71 | message: 'The server said everything was fine but returned a malformed body. This should never happen.'} 72 | ); 73 | } 74 | } 75 | } 76 | 77 | export default OcsConnection; 78 | 79 | module.exports = Object.assign(OcsConnection, { OcsConnection, default: OcsConnection }); 80 | 81 | -------------------------------------------------------------------------------- /compiled/source/webdav.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import * as Webdav from 'webdav-client'; 3 | import * as Stream from 'stream'; 4 | import { FileDetailProperty, ConnectionOptions, FolderProperties, FileDetails } from './types'; 5 | declare function rawGetReadStream(sanePath: string): Promise; 6 | declare function rawRemove(sanePath: string): Promise; 7 | declare function rawExists(sanePath: string): Promise; 8 | declare function rawPut(sanePath: string, content: Webdav.ContentType): Promise; 9 | declare function rawGet(sanePath: string): Promise; 10 | declare function rawGetFiles(sanePath: string): Promise; 11 | declare function rawGetFolderFileDetails(sanePath: string, extraProperties?: FileDetailProperty[]): Promise; 12 | declare function rawGetFolderProperties(sanePath: string, extraProperties?: FileDetailProperty[]): Promise; 13 | declare function rawRename(saneFrom: string, newName: string): Promise; 14 | declare function rawMove(saneFrom: string, toPath: string): Promise; 15 | declare function rawGetWriteStream(sanePath: string): Promise; 16 | declare function rawTouchFolder(sanePath: string): Promise; 17 | declare function rawCreateFolderHierarchy(sanePath: string): Promise; 18 | export declare function configureWebdavConnection(options: ConnectionOptions): void; 19 | export declare function checkConnectivity(): Promise; 20 | declare function rawPipeStream(saneTargetPath: string, readStream: Stream.Readable): Promise; 21 | declare function rawUploadFromStream(saneTargetPath: string, readStream: Stream.Readable): Promise; 22 | declare function rawDownloadToStream(saneSourcePath: string, writeStream: Stream.Writable): Promise; 23 | export declare const createFolderHierarchy: typeof rawCreateFolderHierarchy; 24 | export declare const getFolderFileDetails: typeof rawGetFolderFileDetails; 25 | export declare const getFolderProperties: typeof rawGetFolderProperties; 26 | export declare const getWriteStream: typeof rawGetWriteStream; 27 | export declare const getReadStream: typeof rawGetReadStream; 28 | export declare const touchFolder: typeof rawTouchFolder; 29 | export declare const pipeStream: typeof rawPipeStream; 30 | export declare const uploadFromStream: typeof rawUploadFromStream; 31 | export declare const downloadToStream: typeof rawDownloadToStream; 32 | export declare const getFiles: typeof rawGetFiles; 33 | export declare const rename: typeof rawRename; 34 | export declare const remove: typeof rawRemove; 35 | export declare const move: typeof rawMove; 36 | export declare const exists: typeof rawExists; 37 | export declare const put: typeof rawPut; 38 | export declare const get: typeof rawGet; 39 | export {}; 40 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-config-airbnb", "tslint-eslint-rules"], 3 | 4 | "linterOptions": { 5 | "exclude": [ 6 | "package.json" 7 | ] 8 | }, 9 | 10 | "rules": { 11 | "no-consecutive-blank-lines": [true, 2], 12 | "strict-boolean-expressions": false, 13 | "ter-prefer-arrow-callback": false, 14 | "no-increment-decrement": false, 15 | "prefer-array-literal": false, 16 | "object-curly-spacing": false, 17 | "ter-arrow-parens": false, 18 | "max-line-length": false, 19 | "comment-format": false, 20 | "trailing-comma": false, 21 | "variable-name": false, 22 | "function-name": false, 23 | "prefer-const": false, 24 | "import-name": false, 25 | "ter-indent": false, 26 | "align": false, 27 | "no-this-assignment": false, 28 | 29 | "new-parens": false, 30 | "no-console": false, 31 | "arrow-parens": false, 32 | "no-multi-spaces": false, 33 | "no-unused-expression": false, 34 | "no-use-before-define": ["error", { "functions": false }], 35 | "no-restricted-properties": ["error", { "property" : "lenght" }], 36 | "no-mixed-operators": ["error", { "allowSamePrecedence": true }], 37 | "no-underscore-dangle": ["error", { "allow": ["__", "_id", "_watcher"] }], 38 | "max-len": ["error", { "code": 140, "tabWidth": 2, "ignoreUrls": true }], 39 | "whitespace": [true, "check-preblock"] 40 | }, 41 | 42 | "jsRules": { 43 | "no-consecutive-blank-lines": [true, 2], 44 | "strict-boolean-expressions": false, 45 | "ter-prefer-arrow-callback": false, 46 | "no-increment-decrement": false, 47 | "prefer-array-literal": false, 48 | "variable-name": false, 49 | "function-name": false, 50 | "prefer-const": false, 51 | "import-name": false, 52 | 53 | "new-parens": false, 54 | "no-console": false, 55 | "arrow-parens": false, 56 | "no-multi-spaces": false, 57 | "no-unused-expression": false, 58 | "no-use-before-define": ["error", { "functions": false }], 59 | "no-restricted-properties": ["error", { "property" : "lenght" }], 60 | "no-mixed-operators": ["error", { "allowSamePrecedence": true }], 61 | "no-underscore-dangle": ["error", { "allow": ["__", "_id", "_watcher"] }], 62 | "max-len": ["error", { "code": 140, "tabWidth": 2, "ignoreUrls": true }] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /compiled/source/ocs/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface OcsActivity { 2 | activityId: number; 3 | app: string; 4 | type: string; 5 | user: string; 6 | subject: string; 7 | subjectRich: []; 8 | message: string; 9 | messageRich: []; 10 | objectType: string; 11 | fileId: number; 12 | objectName: string; 13 | objects: {}; 14 | link: string; 15 | icon: string; 16 | datetime: Date; 17 | } 18 | export interface OcsUser { 19 | id: string; 20 | enabled: boolean; 21 | lastLogin: number; 22 | email: string; 23 | displayname: string; 24 | phone: string; 25 | address: string; 26 | website: string; 27 | twitter: string; 28 | groups: string[]; 29 | language: string; 30 | locale: string; 31 | } 32 | export interface OcsNewUser { 33 | userid: string; 34 | password?: string; 35 | email?: string; 36 | displayName?: string; 37 | groups?: string[]; 38 | subadmin?: string[]; 39 | quota?: number; 40 | language?: string; 41 | } 42 | export declare type OcsEditUserField = 'password' | 'email' | 'displayname' | 'quota' | 'phone' | 'address' | 'website' | 'twitter' | 'locale' | 'language'; 43 | export interface OcsHttpError { 44 | code: number; 45 | message: string; 46 | meta?: { 47 | status: string; 48 | statuscode: number; 49 | message: string; 50 | }; 51 | } 52 | export declare enum OcsShareType { 53 | user = 0, 54 | group = 1, 55 | publicLink = 3, 56 | federatedCloudShare = 6 57 | } 58 | export declare enum OcsSharePermissions { 59 | default = -1, 60 | read = 1, 61 | update = 2, 62 | create = 4, 63 | delete = 8, 64 | share = 16, 65 | all = 31 66 | } 67 | export interface OcsShare { 68 | id: number; 69 | shareType: OcsShareType; 70 | shareTypeSystemName: string; 71 | ownerUserId: string; 72 | ownerDisplayName: string; 73 | permissions: OcsSharePermissions; 74 | permissionsText: string; 75 | sharedOn: Date; 76 | sharedOnTimestamp: number; 77 | parent: string; 78 | expiration: Date; 79 | token: string; 80 | fileOwnerUserId: string; 81 | fileOwnerDisplayName: string; 82 | note: string; 83 | label: string; 84 | path: string; 85 | itemType: 'file' | 'folder'; 86 | mimeType: string; 87 | storageId: string; 88 | storage: number; 89 | fileId: number; 90 | parentFileId: number; 91 | fileTarget: string; 92 | sharedWith: string; 93 | sharedWithDisplayName: string; 94 | mailSend: boolean; 95 | hideDownload: boolean; 96 | password?: string; 97 | sendPasswordByTalk?: boolean; 98 | url?: string; 99 | } 100 | export declare type OcsEditShareField = 'permissions' | 'password' | 'publicUpload' | 'expireDate' | 'note'; 101 | export interface OcsGroupfolderManageRule { 102 | type: 'group' | 'user'; 103 | id: string; 104 | displayname: string; 105 | } 106 | export interface OcsGroupfolder { 107 | id: number; 108 | mountPoint: string; 109 | groups: Record; 110 | quota: number; 111 | size: number; 112 | acl: boolean; 113 | manage?: OcsGroupfolderManageRule[]; 114 | } 115 | -------------------------------------------------------------------------------- /source/ocs/types.ts: -------------------------------------------------------------------------------- 1 | export interface OcsActivity { 2 | activityId: number; 3 | app: string; 4 | type: string; 5 | user: string; 6 | subject: string; 7 | subjectRich: []; 8 | message: string; 9 | messageRich: []; 10 | objectType: string; 11 | fileId: number; 12 | objectName: string; 13 | objects: {}; 14 | link: string; 15 | icon: string; 16 | datetime: Date; 17 | } 18 | 19 | export interface OcsUser { 20 | id: string; 21 | enabled: boolean; 22 | lastLogin: number; 23 | email: string; 24 | displayname: string; 25 | phone: string; 26 | address: string; 27 | website: string; 28 | twitter: string; 29 | groups: string[]; 30 | language: string; 31 | locale: string; 32 | } 33 | 34 | export interface OcsNewUser { 35 | userid: string; 36 | password?: string; 37 | email?: string; 38 | displayName?: string; 39 | groups?: string[]; 40 | subadmin?: string[]; 41 | quota?: number; 42 | language?: string; 43 | } 44 | 45 | export type OcsEditUserField = 46 | 'password' | 47 | 'email' | 48 | 'displayname' | 49 | 'quota' | 50 | 'phone' | 51 | 'address' | 52 | 'website' | 53 | 'twitter' | 54 | 'locale' | 55 | 'language' ; 56 | 57 | export interface OcsHttpError { 58 | code: number; 59 | message: string; 60 | meta?: { 61 | status: string; 62 | statuscode: number; 63 | message: string; 64 | }; 65 | } 66 | 67 | export enum OcsShareType { 68 | user = 0, 69 | group = 1, 70 | publicLink = 3, 71 | federatedCloudShare = 6, 72 | } 73 | 74 | export enum OcsSharePermissions { 75 | default = -1, 76 | read = 1, 77 | update = 2, 78 | create = 4, 79 | delete = 8, 80 | share = 16, 81 | all = 31, 82 | } 83 | 84 | export interface OcsShare { 85 | id: number; 86 | shareType: OcsShareType; 87 | shareTypeSystemName: string; 88 | ownerUserId: string; 89 | ownerDisplayName: string; 90 | permissions: OcsSharePermissions; 91 | permissionsText: string; 92 | sharedOn: Date; 93 | sharedOnTimestamp: number; 94 | parent: string; 95 | expiration: Date; 96 | token: string; 97 | fileOwnerUserId: string; 98 | fileOwnerDisplayName: string; 99 | note: string; 100 | label: string; 101 | path: string; 102 | itemType: 'file' | 'folder'; 103 | mimeType: string; 104 | storageId: string; 105 | storage: number; 106 | fileId: number; 107 | parentFileId: number; 108 | fileTarget: string; 109 | sharedWith: string; 110 | sharedWithDisplayName: string; 111 | mailSend: boolean; 112 | hideDownload: boolean; 113 | password?: string; 114 | sendPasswordByTalk?: boolean; 115 | url?: string; 116 | } 117 | 118 | export type OcsEditShareField = 119 | 'permissions' | 120 | 'password' | 121 | 'publicUpload' | 122 | 'expireDate' | 123 | 'note' ; 124 | 125 | export interface OcsGroupfolderManageRule { 126 | type: 'group' | 'user' 127 | id: string; 128 | displayname: string; 129 | } 130 | 131 | export interface OcsGroupfolder { 132 | id: number; 133 | mountPoint: string; 134 | groups: Record; 135 | quota: number; 136 | size: number; 137 | acl: boolean; 138 | manage?: OcsGroupfolderManageRule[]; 139 | } 140 | -------------------------------------------------------------------------------- /compiled/helper.js: -------------------------------------------------------------------------------- 1 | module.exports=function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=33)}({28:function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.OcsError=t.NotReadyError=t.NotFoundError=t.ForbiddenError=t.Exception=void 0;var n=r(52);t.Exception=n(),t.ForbiddenError=n((function(e,t){e.message="Access to "+t+" was denied"}),t.Exception),t.NotFoundError=n((function(e,t){e.message=t+" not found!"}),t.Exception),t.NotReadyError=n((function(e){e.message="The Nextcloud instance is initializing…"}),t.Exception),t.OcsError=n((function(e,t){var r=t.message,n=t.identifier,o=t.reason,i=t.statusCode,u=n?" '"+n+"'":"";e.name="OcsError",e.message=""+r+u+": "+o,i&&(e.statusCode=i)}),t.Exception)},33:function(e,t,r){"use strict";var n=this&&this.__awaiter||function(e,t,r,n){return new(r||(r=Promise))((function(o,i){function u(e){try{a(n.next(e))}catch(e){i(e)}}function c(e){try{a(n.throw(e))}catch(e){i(e)}}function a(e){var t;e.done?o(e.value):(t=e.value,t instanceof r?t:new r((function(e){e(t)}))).then(u,c)}a((n=n.apply(e,t||[])).next())}))},o=this&&this.__generator||function(e,t){var r,n,o,i,u={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:c(0),throw:c(1),return:c(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function c(i){return function(c){return function(i){if(r)throw new TypeError("Generator is already executing.");for(;u;)try{if(r=1,n&&(o=2&i[0]?n.return:i[0]?n.throw||((o=n.return)&&o.call(n),0):n.next)&&!(o=o.call(n,i[1])).done)return o;switch(n=0,o&&(i=[2&i[0],o.value]),i[0]){case 0:case 1:o=i;break;case 4:return u.label++,{value:i[1],done:!1};case 5:u.label++,n=i[1],i=[0];continue;case 7:i=u.ops.pop(),u.trys.pop();continue;default:if(!(o=u.trys,(o=o.length>0&&o[o.length-1])||6!==i[0]&&2!==i[0])){u=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1] void 17 | ): void { 18 | const self: OcsConnection = this; 19 | 20 | const params = { 21 | format: 'json', 22 | }; 23 | 24 | if (search) { 25 | params['search'] = search; 26 | } 27 | 28 | if (limit > -1) { 29 | params['limit'] = limit; 30 | } 31 | 32 | if (offset > -1) { 33 | params['offset'] = offset; 34 | } 35 | 36 | const urlParams = querystring.stringify(params); 37 | 38 | req({ 39 | url: `${self.options.url}/${baseUrl}?${urlParams}`, 40 | headers: self.getHeader() 41 | }, (error, response, body) => { 42 | self.request(error, response, body, (error: OcsHttpError, body?) => { 43 | let result: string[] = null; 44 | 45 | if (!error && body && body.data && body.data.groups) { 46 | result = []; 47 | body.data.groups.forEach(group => { 48 | result.push(group); 49 | }); 50 | } 51 | 52 | callback(error, result); 53 | }); 54 | }); 55 | } 56 | 57 | export function ocsAddGroup(groupId: string, callback: (error: OcsHttpError, result?: boolean) => void): void { 58 | const self: OcsConnection = this; 59 | 60 | req({ 61 | url: `${self.options.url}/${baseUrl}`, 62 | method: 'POST', 63 | headers: self.getHeader(true), 64 | body: JSON.stringify({ 65 | groupid: groupId 66 | }) 67 | }, (error, response, body) => { 68 | self.request(error, response, body, (error: OcsHttpError, body?) => { 69 | let groupAdded = false; 70 | if (!error && body) { 71 | groupAdded = true; 72 | } 73 | 74 | callback(error, groupAdded); 75 | }); 76 | }); 77 | } 78 | 79 | export function ocsDeleteGroup(groupId: string, callback: (error: OcsHttpError, result?: boolean) => void): void { 80 | const self: OcsConnection = this; 81 | 82 | req({ 83 | url: `${self.options.url}/${baseUrl}/${groupId}`, 84 | method: 'DELETE', 85 | headers: self.getHeader(true) 86 | }, (error, response, body) => { 87 | self.request(error, response, body, (error: OcsHttpError, body?) => { 88 | let groupDeleted = false; 89 | if (!error && body) { 90 | groupDeleted = true; 91 | } 92 | 93 | callback(error, groupDeleted); 94 | }); 95 | }); 96 | } 97 | 98 | export function ocsGetGroupUsers(groupId: string, callback: (error: OcsHttpError, result?: string[]) => void): void { 99 | const self: OcsConnection = this; 100 | 101 | req({ 102 | url: `${self.options.url}/${baseUrl}/${groupId}`, 103 | headers: self.getHeader() 104 | }, (error, response, body) => { 105 | self.request(error, response, body, (error: OcsHttpError, body?) => { 106 | let users: string[] = null; 107 | 108 | if (!error && body && body.data && body.data.users) { 109 | users = []; 110 | body.data.users.forEach(user => { 111 | users.push(user); 112 | }); 113 | } 114 | 115 | callback(error, users); 116 | }); 117 | }); 118 | } 119 | 120 | export function ocsGetGroupSubAdmins(groupId: string, callback: (error: OcsHttpError, result?: string[]) => void): void { 121 | const self: OcsConnection = this; 122 | 123 | req({ 124 | url: `${self.options.url}/${baseUrl}/${groupId}/subadmins`, 125 | headers: self.getHeader() 126 | }, (error, response, body) => { 127 | self.request(error, response, body, (error: OcsHttpError, body?) => { 128 | let subAdmins: string[] = null; 129 | 130 | if (!error && body && body.data) { 131 | subAdmins = []; 132 | body.data.forEach(subAdmin => { 133 | subAdmins.push(subAdmin); 134 | }); 135 | } 136 | 137 | callback(error, subAdmins); 138 | }); 139 | }); 140 | } 141 | -------------------------------------------------------------------------------- /compiled/source/ocs/ocs.d.ts: -------------------------------------------------------------------------------- 1 | import { OcsConnection } from './ocs-connection'; 2 | import { OcsSharePermissions, OcsEditUserField, OcsGroupfolder, OcsShareType, OcsActivity, OcsNewUser, OcsShare, OcsUser } from './types'; 3 | import { ConnectionOptions } from '../types'; 4 | export declare function configureOcsConnection(options: ConnectionOptions): void; 5 | export declare function getActivities(connection: OcsConnection, fileId: number | string, sort?: 'asc' | 'desc', limit?: number, sinceActivityId?: number): Promise; 6 | export declare function getUser(connection: OcsConnection, userId: string): Promise; 7 | export declare function setUserEnabled(connection: OcsConnection, userId: string, isEnabled: boolean): Promise; 8 | export declare function editUser(connection: OcsConnection, userId: string, field: OcsEditUserField, value: string): Promise; 9 | export declare function getUserGroups(connection: OcsConnection, userId: string): Promise; 10 | export declare function getUserSubAdmins(connection: OcsConnection, userId: string): Promise; 11 | export declare function resendUserWelcomeEmail(connection: OcsConnection, userId: string): Promise; 12 | export declare function addRemoveUserForGroup(connection: OcsConnection, userId: string, groupId: string, toAdd: boolean): Promise; 13 | export declare function addRemoveUserSubAdminForGroup(connection: OcsConnection, userId: string, groupId: string, toAdd: boolean): Promise; 14 | export declare function listUsers(connection: OcsConnection, search?: string, limit?: number, offset?: number): Promise; 15 | export declare function deleteUser(connection: OcsConnection, userId: string): Promise; 16 | export declare function addUser(connection: OcsConnection, user: OcsNewUser): Promise; 17 | export declare function listGroups(connection: OcsConnection, search?: string, limit?: number, offset?: number): Promise; 18 | export declare function addGroup(connection: OcsConnection, groupId: string): Promise; 19 | export declare function deleteGroup(connection: OcsConnection, groupId: string): Promise; 20 | export declare function getGroupUsers(connection: OcsConnection, groupId: string): Promise; 21 | export declare function getGroupSubAdmins(connection: OcsConnection, groupId: string): Promise; 22 | export declare function getShares(connection: OcsConnection, path?: string, includeReshares?: boolean, showForSubFiles?: boolean): Promise; 23 | export declare function getShare(connection: OcsConnection, shareId: number | string): Promise; 24 | export declare function deleteShare(connection: OcsConnection, shareId: number | string): Promise; 25 | export declare function addShare(connection: OcsConnection, path: string, shareType: OcsShareType, shareWith?: string, permissions?: OcsSharePermissions, password?: string, publicUpload?: boolean): Promise; 26 | export declare function editShare(connection: OcsConnection, shareId: number | string): { 27 | permissions(permissions: OcsSharePermissions): Promise; 28 | password(password: string): Promise; 29 | publicUpload(isPublicUpload: boolean): Promise; 30 | expireDate(expireDate: string): Promise; 31 | note(note: string): Promise; 32 | }; 33 | export declare function getGroupfolders(connection: OcsConnection): Promise; 34 | export declare function getGroupfolder(connection: OcsConnection, groupfolderId: number): Promise; 35 | export declare function addGroupfolder(connection: OcsConnection, mountpoint: string): Promise; 36 | export declare function removeGroupfolder(connection: OcsConnection, groupfolderId: number): Promise; 37 | export declare function addGroupfolderGroup(connection: OcsConnection, groupfolderId: number, groupId: string): Promise; 38 | export declare function removeGroupfolderGroup(connection: OcsConnection, groupfolderId: number, groupId: string): Promise; 39 | export declare function setGroupfolderPermissions(connection: OcsConnection, groupfolderId: number, groupId: string, permissions: number): Promise; 40 | export declare function enableGroupfolderACL(connection: OcsConnection, groupfolderId: number, enable: boolean): Promise; 41 | export declare function setGroupfolderManageACL(connection: OcsConnection, groupfolderId: number, type: 'group' | 'user', id: string, manageACL: boolean): Promise; 42 | export declare function setGroupfolderQuota(connection: OcsConnection, groupfolderId: number, quota: number): Promise; 43 | export declare function renameGroupfolder(connection: OcsConnection, groupfolderId: number, mountpoint: string): Promise; 44 | -------------------------------------------------------------------------------- /compiled/source/client.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import * as Webdav from 'webdav-client'; 3 | import * as Stream from 'stream'; 4 | import { configureWebdavConnection, checkConnectivity } from './webdav'; 5 | import { getCreatorByFileId, getCreatorByPath } from './common'; 6 | import { configureOcsConnection } from './ocs/ocs'; 7 | import { OcsSharePermissions, OcsEditUserField, OcsShareType, OcsNewUser, OcsUser } from './ocs/types'; 8 | import { NextcloudClientProperties, NextcloudClientInterface, ConnectionOptions } from './types'; 9 | export declare class NextcloudClient extends NextcloudClientProperties implements NextcloudClientInterface { 10 | configureWebdavConnection: typeof configureWebdavConnection; 11 | configureOcsConnection: typeof configureOcsConnection; 12 | createFolderHierarchy: (sanePath: string) => Promise; 13 | getFolderFileDetails: (sanePath: string, extraProperties?: Webdav.ConnectionReaddirProperty[]) => Promise; 14 | getFolderProperties: (sanePath: string, extraProperties?: Webdav.ConnectionReaddirProperty[]) => Promise; 15 | checkConnectivity: typeof checkConnectivity; 16 | downloadToStream: (saneSourcePath: string, writeStream: Stream.Writable) => Promise; 17 | uploadFromStream: (saneTargetPath: string, readStream: Stream.Readable) => Promise; 18 | getWriteStream: (sanePath: string) => Promise; 19 | getReadStream: (sanePath: string) => Promise; 20 | touchFolder: (sanePath: string) => Promise; 21 | pipeStream: (saneTargetPath: string, readStream: Stream.Readable) => Promise; 22 | getFiles: (sanePath: string) => Promise; 23 | rename: (saneFrom: string, newName: string) => Promise; 24 | remove: (sanePath: string) => Promise; 25 | exists: (sanePath: string) => Promise; 26 | move: (saneFrom: string, toPath: string) => Promise; 27 | put: (sanePath: string, content: Webdav.ContentType) => Promise; 28 | get: (sanePath: string) => Promise; 29 | getCreatorByFileId: typeof getCreatorByFileId; 30 | getCreatorByPath: typeof getCreatorByPath; 31 | activities: { 32 | get: (fileId: number | string, sort?: 'asc' | 'desc', limit?: number, sinceActivityId?: number) => Promise; 33 | }; 34 | users: { 35 | removeSubAdminFromGroup: (userId: string, groupId: string) => Promise; 36 | addSubAdminToGroup: (userId: string, groupId: string) => Promise; 37 | resendWelcomeEmail: (userId: string) => Promise; 38 | getSubAdminGroups: (userId: string) => Promise; 39 | removeFromGroup: (userId: string, groupId: string) => Promise; 40 | setEnabled: (userId: string, isEnabled: boolean) => Promise; 41 | addToGroup: (userId: string, groupId: string) => Promise; 42 | getGroups: (userId: string) => Promise; 43 | delete: (userId: string) => Promise; 44 | edit: (userId: string, field: OcsEditUserField, value: string) => Promise; 45 | list: (search?: string, limit?: number, offset?: number) => Promise; 46 | add: (user: OcsNewUser) => Promise; 47 | get: (userId: string) => Promise; 48 | }; 49 | groups: { 50 | getSubAdmins: (groupId: string) => Promise; 51 | getUsers: (groupId: string) => Promise; 52 | delete: (groupId: string) => Promise; 53 | list: (search?: string, limit?: number, offset?: number) => Promise; 54 | add: (groupId: string) => Promise; 55 | }; 56 | shares: { 57 | delete: (shareId: number | string) => Promise; 58 | edit: { 59 | permissions: (shareId: number | string, permissions: OcsSharePermissions) => Promise; 60 | password: (shareId: number | string, password: string) => Promise; 61 | publicUpload: (shareId: number | string, isPublicUpload: boolean) => Promise; 62 | expireDate: (shareId: number | string, expireDate: string) => Promise; 63 | note: (shareId: number | string, note: string) => Promise; 64 | }; 65 | list: (path?: string, includeReshares?: boolean, showForSubFiles?: boolean) => Promise; 66 | add: (path: string, shareType: OcsShareType, shareWith?: string, permissions?: OcsSharePermissions, password?: string, publicUpload?: boolean) => Promise; 67 | get: (shareId: number | string) => Promise; 68 | }; 69 | groupfolders: { 70 | getFolders: () => Promise; 71 | getFolder: (fid: number) => Promise; 72 | addFolder: (mountpoint: string) => Promise; 73 | removeFolder: (fid: number) => Promise; 74 | addGroup: (fid: number, gid: string) => Promise; 75 | removeGroup: (fid: number, gid: string) => Promise; 76 | setPermissions: (fid: number, gid: string, permissions: number) => Promise; 77 | enableACL: (fid: number, enable: boolean) => Promise; 78 | setManageACL: (fid: number, type: 'group' | 'user', id: string, manageACL: boolean) => Promise; 79 | setQuota: (fid: number, quota: number) => Promise; 80 | renameFolder: (fid: number, mountpoint: string) => Promise; 81 | }; 82 | constructor(options: ConnectionOptions); 83 | as(username: string, password: string): NextcloudClient; 84 | } 85 | export default NextcloudClient; 86 | -------------------------------------------------------------------------------- /compiled/source/types.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { OcsConnection } from './ocs/ocs-connection'; 3 | import * as Stream from 'stream'; 4 | import * as Webdav from 'webdav-client'; 5 | import { OcsSharePermissions, OcsEditUserField, OcsGroupfolder, OcsShareType, OcsActivity, OcsNewUser, OcsShare, OcsUser } from './ocs/types'; 6 | export * from './ocs/types'; 7 | export declare type AsyncFunction = (...parameters: any[]) => Promise; 8 | export declare type FileDetails = Webdav.ConnectionReaddirComplexResult; 9 | export declare type FileDetailProperty = Webdav.ConnectionReaddirProperty; 10 | export declare type FolderProperties = Webdav.Properties; 11 | export declare class NextcloudClientProperties { 12 | webdavConnection: Webdav.Connection; 13 | ocsConnection: OcsConnection; 14 | username: string; 15 | url: string; 16 | } 17 | export interface NextcloudClientInterface extends NextcloudClientProperties { 18 | getFolderFileDetails(path: string, extraProperties?: FileDetailProperty[]): Promise; 19 | getFolderProperties(path: string, extraProperties?: FileDetailProperty[]): Promise; 20 | configureWebdavConnection(options: ConnectionOptions): void; 21 | configureOcsConnection(options: ConnectionOptions): void; 22 | pipeStream(path: string, readStream: Stream.Readable): Promise; 23 | uploadFromStream(targetPath: string, readStream: Stream.Readable): Promise; 24 | downloadToStream(sourcePath: string, writeStream: Stream.Writable): Promise; 25 | rename(fromFullPath: string, toFileName: string): Promise; 26 | move(fromFullPath: string, toFullPath: string): Promise; 27 | as(username: string, password: string): NextcloudClientInterface; 28 | createFolderHierarchy(path: string): Promise; 29 | put(path: string, content: Webdav.ContentType): Promise; 30 | getWriteStream(path: string): Promise; 31 | getReadStream(path: string): Promise; 32 | touchFolder(path: string): Promise; 33 | getFiles(path: string): Promise; 34 | remove(path: string): Promise; 35 | exists(path: string): Promise; 36 | checkConnectivity(): Promise; 37 | get(path: string): Promise; 38 | getCreatorByFileId(fileId: number | string): Promise; 39 | getCreatorByPath(path: string): Promise; 40 | activities: { 41 | get: (fileId: number | string, sort?: 'asc' | 'desc', limit?: number, sinceActivityId?: number) => Promise; 42 | }; 43 | users: { 44 | removeSubAdminFromGroup: (userId: string, groupId: string) => Promise; 45 | addSubAdminToGroup: (userId: string, groupId: string) => Promise; 46 | resendWelcomeEmail: (userId: string) => Promise; 47 | getSubAdminGroups: (userId: string) => Promise; 48 | removeFromGroup: (userId: string, groupId: string) => Promise; 49 | setEnabled: (userId: string, isEnabled: boolean) => Promise; 50 | addToGroup: (userId: string, groupId: string) => Promise; 51 | getGroups: (userId: string) => Promise; 52 | delete: (userId: string) => Promise; 53 | edit: (userId: string, field: OcsEditUserField, value: string) => Promise; 54 | list: (search?: string, limit?: number, offset?: number) => Promise; 55 | add: (user: OcsNewUser) => Promise; 56 | get: (userId: string) => Promise; 57 | }; 58 | groups: { 59 | getSubAdmins: (groupId: string) => Promise; 60 | getUsers: (groupId: string) => Promise; 61 | delete: (groupId: string) => Promise; 62 | list: (search?: string, limit?: number, offset?: number) => Promise; 63 | add: (groupId: string) => Promise; 64 | }; 65 | shares: { 66 | delete: (shareId: string | number) => Promise; 67 | edit: { 68 | permissions: (shareId: string | number, permissions: OcsSharePermissions) => Promise; 69 | password: (shareId: string | number, password: string) => Promise; 70 | publicUpload: (shareId: string | number, isPublicUpload: boolean) => Promise; 71 | expireDate: (shareId: string | number, expireDate: string) => Promise; 72 | note: (shareId: string | number, note: string) => Promise; 73 | }; 74 | list: (path?: string, includeReshares?: boolean, showForSubFiles?: boolean) => Promise; 75 | add: (path: string, shareType: OcsShareType, shareWith?: string, permissions?: OcsSharePermissions, password?: string, publicUpload?: boolean) => Promise; 76 | get: (shareId: string | number) => Promise; 77 | }; 78 | groupfolders: { 79 | getFolders: () => Promise; 80 | getFolder: (fid: number) => Promise; 81 | addFolder: (mountpoint: string) => Promise; 82 | removeFolder: (fid: number) => Promise; 83 | addGroup: (fid: number, gid: string) => Promise; 84 | removeGroup: (fid: number, gid: string) => Promise; 85 | setPermissions: (fid: number, gid: string, permissions: number) => Promise; 86 | enableACL: (fid: number, enable: boolean) => Promise; 87 | setManageACL: (fid: number, type: 'group' | 'user', id: string, manageACL: boolean) => Promise; 88 | setQuota: (fid: number, quota: number) => Promise; 89 | renameFolder: (fid: number, mountpoint: string) => Promise; 90 | }; 91 | } 92 | export interface ConnectionOptions { 93 | url: string; 94 | username?: string; 95 | password?: string; 96 | } 97 | -------------------------------------------------------------------------------- /source/ocs/share.ts: -------------------------------------------------------------------------------- 1 | import * as querystring from 'querystring'; 2 | import req from 'request'; 3 | 4 | import { 5 | OcsSharePermissions, 6 | OcsEditShareField, 7 | OcsHttpError, 8 | OcsShareType, 9 | OcsShare, 10 | } from './types'; 11 | 12 | import { OcsConnection } from './ocs-connection'; 13 | import { assignDefined, ocsSharePermissionsToText } from './helper'; 14 | 15 | const baseUrl = 'ocs/v2.php/apps/files_sharing/api/v1/shares'; 16 | 17 | export function ocsGetShares( 18 | path: string, 19 | includeReshares: boolean, 20 | showForSubFiles: boolean, 21 | callback: (error: OcsHttpError, result?: OcsShare[]) => void 22 | ): void { 23 | const self: OcsConnection = this; 24 | 25 | const params = { 26 | format: 'json' 27 | }; 28 | 29 | if (path) { 30 | params['path'] = path; 31 | params['reshares'] = includeReshares; 32 | params['subfiles'] = showForSubFiles; 33 | } 34 | 35 | const urlParams = querystring.stringify(params); 36 | 37 | req({ 38 | url: `${self.options.url}/${baseUrl}?${urlParams}`, 39 | headers: self.getHeader() 40 | }, (error, response, body) => { 41 | self.request(error, response, body, (error: OcsHttpError, body?) => { 42 | let result: OcsShare[] = null; 43 | 44 | if (!error && body && body.data) { 45 | result = []; 46 | body.data.forEach(share => { 47 | result.push(parseOcsShare(share)); 48 | }); 49 | } 50 | 51 | callback(error, result); 52 | }); 53 | }); 54 | } 55 | 56 | export function ocsGetShare( 57 | shareId: number | string, 58 | callback: (error: OcsHttpError, result?: OcsShare) => void 59 | ): void { 60 | const self: OcsConnection = this; 61 | 62 | req({ 63 | url: `${self.options.url}/${baseUrl}/${shareId}`, 64 | headers: self.getHeader() 65 | }, (error, response, body) => { 66 | self.request(error, response, body, (error: OcsHttpError, body?) => { 67 | let result: OcsShare = null; 68 | 69 | if (!error && body && body.data && body.data.length > 0) { 70 | result = parseOcsShare(body.data[0]); 71 | } 72 | 73 | callback(error, result); 74 | }); 75 | }); 76 | } 77 | 78 | export function ocsDeleteShare( 79 | shareId: number | string, 80 | callback: (error: OcsHttpError, result?: boolean) => void 81 | ): void { 82 | const self: OcsConnection = this; 83 | 84 | req({ 85 | url: `${self.options.url}/${baseUrl}/${shareId}`, 86 | method: 'DELETE', 87 | headers: self.getHeader() 88 | }, (error, response, body) => { 89 | self.request(error, response, body, (error: OcsHttpError, body?) => { 90 | let shareDeleted = false; 91 | if (!error && body) { 92 | shareDeleted = true; 93 | } 94 | 95 | callback(error, shareDeleted); 96 | }); 97 | }); 98 | } 99 | 100 | export function ocsAddShare( 101 | path: string, 102 | shareType: OcsShareType, 103 | shareWith: string, 104 | permissions: OcsSharePermissions, 105 | password: string, 106 | publicUpload: boolean, 107 | callback: (error: OcsHttpError, result?: OcsShare) => void 108 | ): void { 109 | const self: OcsConnection = this; 110 | 111 | const share = { 112 | path, 113 | shareType, 114 | }; 115 | 116 | share['publicUpload'] = String(publicUpload); 117 | 118 | if (shareWith) { 119 | share['shareWith'] = shareWith; 120 | } 121 | 122 | if (permissions && permissions !== OcsSharePermissions.default) { 123 | share['permissions'] = permissions; 124 | } 125 | 126 | if (password) { 127 | share['password'] = password; 128 | } 129 | 130 | req({ 131 | url: `${self.options.url}/${baseUrl}`, 132 | method: 'POST', 133 | headers: self.getHeader(true), 134 | body: JSON.stringify(share) 135 | }, (error, response, body) => { 136 | self.request(error, response, body, (error: OcsHttpError, body?) => { 137 | let result: OcsShare = null; 138 | if (!error && body && body.data) { 139 | result = parseOcsShare(body.data); 140 | } 141 | 142 | callback(error, result); 143 | }); 144 | }); 145 | } 146 | 147 | export function ocsEditShare( 148 | shareId: number | string, 149 | field: OcsEditShareField, 150 | value: string, 151 | callback: (error: OcsHttpError, result?: OcsShare) => void 152 | ) { 153 | const self: OcsConnection = this; 154 | 155 | req({ 156 | url: `${self.options.url}/${baseUrl}/${shareId}`, 157 | method: 'PUT', 158 | headers: self.getHeader(true), 159 | body: JSON.stringify({ [field]: value }) 160 | }, (error, response, body) => { 161 | self.request(error, response, body, (error: OcsHttpError, body?) => { 162 | let result: OcsShare = null; 163 | if (!error && body && body.data) { 164 | result = parseOcsShare(body.data); 165 | } 166 | 167 | callback(error, result); 168 | }); 169 | }); 170 | } 171 | 172 | function parseOcsShare(share) { 173 | const timestamp = parseInt(share.stime, 10); 174 | const permissionsInt = parseInt(share.permissions, 10); 175 | const shareTypeInt = parseInt(share.share_type, 10); 176 | 177 | const obj : OcsShare = { 178 | id: parseInt(share.id, 10), 179 | shareType: shareTypeInt, 180 | shareTypeSystemName: OcsShareType[shareTypeInt], 181 | ownerUserId: share.uid_owner, 182 | ownerDisplayName: share.displayname_owner, 183 | permissions: permissionsInt, 184 | permissionsText: ocsSharePermissionsToText(permissionsInt), 185 | sharedOn: new Date(timestamp * 1000), 186 | sharedOnTimestamp: timestamp, 187 | parent: share.parent, 188 | expiration: share.expiration, 189 | token: share.token, 190 | fileOwnerUserId: share.uid_file_owner, 191 | fileOwnerDisplayName: share.displayname_file_owner, 192 | note: share.note, 193 | label: share.label, 194 | path: share.path, 195 | itemType: share.item_type, 196 | mimeType: share.mimetype, 197 | storageId: share.storage_id, 198 | storage: parseInt(share.storage, 10), 199 | fileId: parseInt(share.item_source, 10), 200 | parentFileId: parseInt(share.file_parent, 10), 201 | fileTarget: share.file_target, 202 | sharedWith: share.share_with, 203 | sharedWithDisplayName: share.share_with_displayname, 204 | mailSend: Boolean(share.mail_send), 205 | hideDownload: Boolean(share.hide_download), 206 | }; 207 | 208 | assignDefined(obj, { 209 | password: share.password, 210 | sendPasswordByTalk: share.send_password_by_talk, 211 | url: share.url, 212 | }); 213 | 214 | return obj; 215 | } 216 | -------------------------------------------------------------------------------- /source/types.ts: -------------------------------------------------------------------------------- 1 | import { OcsConnection } from './ocs/ocs-connection'; 2 | import * as Stream from 'stream'; 3 | import * as Webdav from 'webdav-client'; 4 | import { 5 | OcsSharePermissions, 6 | OcsEditUserField, 7 | OcsGroupfolder, 8 | OcsShareType, 9 | OcsActivity, 10 | OcsNewUser, 11 | OcsShare, 12 | OcsUser, 13 | } from './ocs/types'; 14 | 15 | export * from './ocs/types'; 16 | 17 | export type AsyncFunction = (...parameters: any[]) => Promise; 18 | export type FileDetails = Webdav.ConnectionReaddirComplexResult; 19 | export type FileDetailProperty = Webdav.ConnectionReaddirProperty; 20 | export type FolderProperties = Webdav.Properties; 21 | 22 | export class NextcloudClientProperties { 23 | webdavConnection: Webdav.Connection; 24 | ocsConnection: OcsConnection; 25 | username: string; 26 | url: string; 27 | } 28 | 29 | export interface NextcloudClientInterface extends NextcloudClientProperties { 30 | getFolderFileDetails(path: string, extraProperties?: FileDetailProperty[]): Promise; 31 | getFolderProperties(path: string, extraProperties?: FileDetailProperty[]): Promise; 32 | configureWebdavConnection(options: ConnectionOptions): void; 33 | configureOcsConnection(options: ConnectionOptions): void; 34 | pipeStream(path: string, readStream: Stream.Readable): Promise; 35 | uploadFromStream(targetPath: string, readStream: Stream.Readable): Promise; 36 | downloadToStream(sourcePath: string, writeStream: Stream.Writable): Promise; 37 | rename(fromFullPath: string, toFileName: string): Promise; 38 | move(fromFullPath: string, toFullPath: string): Promise; 39 | as(username: string, password: string): NextcloudClientInterface; 40 | createFolderHierarchy(path: string): Promise; 41 | put(path: string, content: Webdav.ContentType): Promise; 42 | getWriteStream(path: string): Promise; 43 | getReadStream(path: string): Promise; 44 | touchFolder(path: string): Promise; 45 | getFiles(path: string): Promise; 46 | remove(path: string): Promise; 47 | exists(path: string): Promise; 48 | checkConnectivity(): Promise; 49 | get(path: string): Promise; 50 | 51 | // Common 52 | getCreatorByFileId(fileId: number | string): Promise; 53 | getCreatorByPath(path: string): Promise; 54 | 55 | // OCS 56 | activities: { 57 | get: (fileId: number | string, sort?: 'asc' | 'desc', 58 | limit?: number, sinceActivityId?: number) => Promise 59 | }; 60 | users: { 61 | removeSubAdminFromGroup: (userId: string, groupId: string) => Promise 62 | addSubAdminToGroup: (userId: string, groupId: string) => Promise 63 | resendWelcomeEmail: (userId: string) => Promise 64 | getSubAdminGroups: (userId: string) => Promise 65 | removeFromGroup: (userId: string, groupId: string) => Promise 66 | setEnabled: (userId: string, isEnabled: boolean) => Promise 67 | addToGroup: (userId: string, groupId: string) => Promise 68 | getGroups: (userId: string) => Promise 69 | delete: (userId: string) => Promise 70 | edit: (userId: string, field: OcsEditUserField, value: string) => Promise 71 | list: (search?: string, limit?: number, offset?: number) => Promise 72 | add: (user: OcsNewUser) => Promise 73 | get: (userId: string) => Promise 74 | }; 75 | 76 | groups: { 77 | getSubAdmins: (groupId: string) => Promise 78 | getUsers: (groupId: string) => Promise 79 | delete: (groupId: string) => Promise 80 | list: (search?: string, limit?: number, offset?: number) => Promise 81 | add: (groupId: string) => Promise 82 | }; 83 | 84 | shares: { 85 | delete: (shareId: string | number) => Promise 86 | edit: { 87 | permissions: (shareId: string | number, 88 | permissions: OcsSharePermissions) => Promise 89 | password: (shareId: string | number, 90 | password: string) => Promise 91 | publicUpload: (shareId: string | number, 92 | isPublicUpload: boolean) => Promise 93 | expireDate: (shareId: string | number, 94 | expireDate: string) => Promise 95 | note: (shareId: string | number, note: string) => Promise 96 | } 97 | list: (path?: string, includeReshares?: boolean, 98 | showForSubFiles?: boolean) => Promise 99 | add: (path: string, shareType: OcsShareType, shareWith?: string, 100 | permissions?: OcsSharePermissions, password?: string, 101 | publicUpload?: boolean) => Promise 102 | get: (shareId: string | number) => Promise 103 | }; 104 | 105 | groupfolders: { 106 | getFolders: () => Promise 107 | getFolder: (fid: number) => Promise 108 | addFolder: (mountpoint: string) => Promise 109 | removeFolder: (fid: number) => Promise 110 | addGroup: (fid: number, gid: string) => Promise 111 | removeGroup: (fid: number, gid: string) => Promise 112 | setPermissions: (fid: number, gid: string, permissions: number) => Promise 113 | enableACL: (fid: number, enable: boolean) => Promise 114 | setManageACL: (fid: number, type: 'group' | 'user', id: string, manageACL: boolean) => Promise 115 | setQuota: (fid: number, quota: number) => Promise 116 | renameFolder: (fid: number, mountpoint: string) => Promise 117 | }; 118 | } 119 | 120 | export interface ConnectionOptions { 121 | url: string; 122 | username?: string; 123 | password?: string; 124 | } 125 | -------------------------------------------------------------------------------- /source/client.ts: -------------------------------------------------------------------------------- 1 | import * as Webdav from 'webdav-client'; 2 | import * as Stream from 'stream'; 3 | 4 | import { 5 | configureWebdavConnection, 6 | createFolderHierarchy, 7 | getFolderFileDetails, 8 | getFolderProperties, 9 | checkConnectivity, 10 | downloadToStream, 11 | uploadFromStream, 12 | getWriteStream, 13 | getReadStream, 14 | touchFolder, 15 | pipeStream, 16 | getFiles, 17 | rename, 18 | remove, 19 | exists, 20 | move, 21 | put, 22 | get 23 | } from './webdav'; 24 | 25 | import { 26 | getCreatorByFileId, 27 | getCreatorByPath, 28 | } from './common'; 29 | 30 | import { 31 | addRemoveUserSubAdminForGroup, 32 | setGroupfolderPermissions, 33 | setGroupfolderManageACL, 34 | configureOcsConnection, 35 | resendUserWelcomeEmail, 36 | removeGroupfolderGroup, 37 | addRemoveUserForGroup, 38 | enableGroupfolderACL, 39 | addGroupfolderGroup, 40 | setGroupfolderQuota, 41 | getGroupSubAdmins, 42 | renameGroupfolder, 43 | removeGroupfolder, 44 | getUserSubAdmins, 45 | getGroupfolders, 46 | addGroupfolder, 47 | getGroupfolder, 48 | setUserEnabled, 49 | getActivities, 50 | getGroupUsers, 51 | getUserGroups, 52 | deleteGroup, 53 | deleteShare, 54 | deleteUser, 55 | listGroups, 56 | editShare, 57 | getShares, 58 | listUsers, 59 | addGroup, 60 | addShare, 61 | editUser, 62 | getShare, 63 | getUser, 64 | addUser, 65 | } from './ocs/ocs'; 66 | 67 | import { 68 | OcsSharePermissions, 69 | OcsEditUserField, 70 | OcsShareType, 71 | OcsNewUser, 72 | OcsUser, 73 | } from './ocs/types'; 74 | 75 | import { 76 | NextcloudClientProperties, 77 | NextcloudClientInterface, 78 | ConnectionOptions, 79 | AsyncFunction 80 | } from './types'; 81 | import OcsConnection from './ocs/ocs-connection'; 82 | 83 | export class NextcloudClient extends NextcloudClientProperties implements NextcloudClientInterface { 84 | configureWebdavConnection = configureWebdavConnection; 85 | configureOcsConnection = configureOcsConnection; 86 | createFolderHierarchy = createFolderHierarchy; 87 | getFolderFileDetails = getFolderFileDetails; 88 | getFolderProperties = getFolderProperties; 89 | checkConnectivity = checkConnectivity; 90 | downloadToStream = downloadToStream; 91 | uploadFromStream = uploadFromStream; 92 | getWriteStream = getWriteStream; 93 | getReadStream = getReadStream; 94 | touchFolder = touchFolder; 95 | pipeStream = pipeStream; 96 | getFiles = getFiles; 97 | rename = rename; 98 | remove = remove; 99 | exists = exists; 100 | move = move; 101 | put = put; 102 | get = get; 103 | 104 | // Common 105 | getCreatorByFileId = getCreatorByFileId; 106 | getCreatorByPath = getCreatorByPath; 107 | 108 | // OCS 109 | activities = { 110 | get : (fileId: number | string, sort?: 'asc' | 'desc', 111 | limit?: number, sinceActivityId?: number) => getActivities(this.ocsConnection, fileId, sort, limit, sinceActivityId) 112 | }; 113 | 114 | users = { 115 | removeSubAdminFromGroup : (userId: string, groupId: string) => addRemoveUserSubAdminForGroup(this.ocsConnection, userId, groupId, false), 116 | addSubAdminToGroup : (userId: string, groupId: string) => addRemoveUserSubAdminForGroup(this.ocsConnection, userId, groupId, true), 117 | resendWelcomeEmail : (userId: string) => resendUserWelcomeEmail(this.ocsConnection, userId), 118 | getSubAdminGroups : (userId: string) => getUserSubAdmins(this.ocsConnection, userId), 119 | removeFromGroup : (userId: string, groupId: string) => addRemoveUserForGroup(this.ocsConnection, userId, groupId, false), 120 | setEnabled : (userId: string, isEnabled: boolean) => setUserEnabled(this.ocsConnection, userId, isEnabled), 121 | addToGroup : (userId: string, groupId: string) => addRemoveUserForGroup(this.ocsConnection, userId, groupId, true), 122 | getGroups : (userId: string) => getUserGroups(this.ocsConnection, userId), 123 | delete : (userId: string) => deleteUser(this.ocsConnection, userId), 124 | edit : (userId: string, field: OcsEditUserField, value: string) => editUser(this.ocsConnection, userId, field, value), 125 | list : (search?: string, limit?: number, offset?: number) => listUsers(this.ocsConnection, search, limit, offset), 126 | add : (user: OcsNewUser) => addUser(this.ocsConnection, user), 127 | get : (userId: string) => getUser(this.ocsConnection, userId), 128 | }; 129 | 130 | groups = { 131 | getSubAdmins : (groupId: string) => getGroupSubAdmins(this.ocsConnection, groupId), 132 | getUsers : (groupId: string) => getGroupUsers(this.ocsConnection, groupId), 133 | delete : (groupId: string) => deleteGroup(this.ocsConnection, groupId), 134 | list : (search?: string, limit?: number, offset?: number) => listGroups(this.ocsConnection, search, limit, offset), 135 | add : (groupId: string) => addGroup(this.ocsConnection, groupId), 136 | }; 137 | 138 | shares = { 139 | delete : (shareId: number | string) => deleteShare(this.ocsConnection, shareId), 140 | edit: { 141 | permissions : (shareId: number | string, permissions: OcsSharePermissions) => editShare(this.ocsConnection, shareId).permissions(permissions), 142 | password : (shareId: number | string, password: string) => editShare(this.ocsConnection, shareId).password(password), 143 | publicUpload : (shareId: number | string, isPublicUpload: boolean) => editShare(this.ocsConnection, shareId).publicUpload(isPublicUpload), 144 | expireDate : (shareId: number | string, expireDate: string) => editShare(this.ocsConnection, shareId).expireDate(expireDate), 145 | note : (shareId: number | string, note: string) => editShare(this.ocsConnection, shareId).note(note), 146 | }, 147 | list : (path?: string, includeReshares?: boolean, showForSubFiles?: boolean) => getShares(this.ocsConnection, path, includeReshares, showForSubFiles), 148 | add : (path: string, shareType: OcsShareType, shareWith?: string, permissions?: OcsSharePermissions, 149 | password?: string, publicUpload?: boolean) => addShare(this.ocsConnection, path, shareType, shareWith, permissions, password, publicUpload), 150 | get : (shareId: number | string) => getShare(this.ocsConnection, shareId), 151 | }; 152 | 153 | groupfolders = { 154 | getFolders: () => getGroupfolders(this.ocsConnection), 155 | getFolder: (fid: number) => getGroupfolder(this.ocsConnection, fid), 156 | addFolder: (mountpoint: string) => addGroupfolder(this.ocsConnection, mountpoint), 157 | removeFolder: (fid: number) => removeGroupfolder(this.ocsConnection, fid), 158 | addGroup: (fid: number, gid: string) => addGroupfolderGroup(this.ocsConnection, fid, gid), 159 | removeGroup: (fid: number, gid: string) => removeGroupfolderGroup(this.ocsConnection, fid, gid), 160 | setPermissions: (fid: number, gid: string, permissions: number) => setGroupfolderPermissions(this.ocsConnection, fid, gid, permissions), 161 | enableACL: (fid: number, enable: boolean) => enableGroupfolderACL(this.ocsConnection, fid, enable), 162 | setManageACL: (fid: number, type: 'group' | 'user', id: string, manageACL: boolean) => setGroupfolderManageACL(this.ocsConnection, fid, type, id, manageACL), 163 | setQuota: (fid: number, quota: number) => setGroupfolderQuota(this.ocsConnection, fid, quota), 164 | renameFolder: (fid: number, mountpoint: string) => renameGroupfolder(this.ocsConnection, fid, mountpoint), 165 | }; 166 | 167 | constructor(options: ConnectionOptions) { 168 | super(); 169 | 170 | this.username = options.username; 171 | this.url = options.url.endsWith('/') ? options.url.slice(0, -1) : options.url; 172 | 173 | this.configureWebdavConnection(options); 174 | this.configureOcsConnection(options); 175 | } 176 | 177 | as(username: string, password: string): NextcloudClient { 178 | return new NextcloudClient({ username, password, url: this.url }); 179 | } 180 | } 181 | 182 | // Shush, Typescript… 183 | export default NextcloudClient; 184 | 185 | // Support default import syntax for Node and TS, and also a named export. 186 | module.exports = Object.assign(NextcloudClient, { NextcloudClient, default: NextcloudClient }); 187 | -------------------------------------------------------------------------------- /source/ocs/user.ts: -------------------------------------------------------------------------------- 1 | import * as querystring from 'querystring'; 2 | import req from 'request'; 3 | 4 | import { 5 | OcsEditUserField, 6 | OcsHttpError, 7 | OcsNewUser, 8 | OcsUser, 9 | } from './types'; 10 | import { OcsConnection } from './ocs-connection'; 11 | 12 | const baseUrl = 'ocs/v2.php/cloud/users'; 13 | 14 | export function ocsGetUser(userId: string, callback: (error: OcsHttpError, result?: OcsUser) => void) : void { 15 | const self: OcsConnection = this; 16 | 17 | const urlParams = querystring.stringify({ 18 | format: 'json' 19 | }); 20 | 21 | req({ 22 | url: `${self.options.url}/${baseUrl}/${userId}?${urlParams}`, 23 | headers: self.getHeader() 24 | }, (error, response, body) => { 25 | self.request(error, response, body, (error: OcsHttpError, body?) => { 26 | let result: OcsUser = null; 27 | 28 | if (!error && body && body.data) { 29 | result = { 30 | id: body.data.id, 31 | enabled: body.data.enabled, 32 | lastLogin: body.data.lastLogin, 33 | email: body.data.email, 34 | displayname: body.data.displayname, 35 | phone: body.data.phone, 36 | address: body.data.address, 37 | website: body.data.website, 38 | twitter: body.data.twitter, 39 | groups: body.data.groups, 40 | language: body.data.language, 41 | locale: body.data.locale 42 | }; 43 | } 44 | 45 | callback(error, result); 46 | }); 47 | }); 48 | } 49 | 50 | export function ocsListUsers( 51 | search: string, 52 | limit: number, 53 | offset: number, 54 | callback: (error: OcsHttpError, result?: string[]) => void 55 | ): void { 56 | const self: OcsConnection = this; 57 | 58 | const params = { 59 | format: 'json', 60 | }; 61 | 62 | if (search) { 63 | params['search'] = search; 64 | } 65 | 66 | if (limit > -1) { 67 | params['limit'] = limit; 68 | } 69 | 70 | if (offset > -1) { 71 | params['offset'] = offset; 72 | } 73 | 74 | const urlParams = querystring.stringify(params); 75 | 76 | req({ 77 | url: `${self.options.url}/${baseUrl}?${urlParams}`, 78 | headers: self.getHeader() 79 | }, (error, response, body) => { 80 | self.request(error, response, body, (error: OcsHttpError, body?) => { 81 | let users: string[] = null; 82 | 83 | if (!error && body && body.data && body.data.users) { 84 | users = []; 85 | body.data.users.forEach(user => { 86 | users.push(user); 87 | }); 88 | } 89 | 90 | callback(error, users); 91 | }); 92 | }); 93 | } 94 | 95 | export function ocsSetUserEnabled( 96 | userId: string, 97 | isEnabled: boolean, 98 | callback: (error: OcsHttpError, result?: boolean) => void 99 | ): void { 100 | const self: OcsConnection = this; 101 | 102 | req({ 103 | url: `${self.options.url}/${baseUrl}/${userId}/${isEnabled ? 'enable' : 'disable'}`, 104 | method: 'PUT', 105 | headers: self.getHeader() 106 | }, (error, response, body) => { 107 | self.request(error, response, body, (error: OcsHttpError, body?) => { 108 | let success = false; 109 | if (!error && body) { 110 | success = true; 111 | } 112 | 113 | callback(error, success); 114 | }); 115 | }); 116 | } 117 | 118 | export function ocsDeleteUser(userId: string, callback: (error: OcsHttpError, result?: boolean) => void): void { 119 | const self: OcsConnection = this; 120 | 121 | req({ 122 | url: `${self.options.url}/${baseUrl}/${userId}`, 123 | method: 'DELETE', 124 | headers: self.getHeader() 125 | }, (error, response, body) => { 126 | self.request(error, response, body, (error: OcsHttpError, body?) => { 127 | let userDeleted = false; 128 | if (!error && body) { 129 | userDeleted = true; 130 | } 131 | 132 | callback(error, userDeleted); 133 | }); 134 | }); 135 | } 136 | 137 | export function ocsAddUser(user: OcsNewUser, callback: (error: OcsHttpError, result?: boolean) => void): void { 138 | const self: OcsConnection = this; 139 | 140 | // Basic validation 141 | if (!user) { 142 | callback({ code: 0, message: 'must have a valid OcsNewUser object.' }); 143 | return; 144 | } 145 | if (!user.userid) { 146 | callback({ code: 0, message: 'user must have an id.' }); 147 | return; 148 | } 149 | 150 | req({ 151 | url: `${self.options.url}/${baseUrl}`, 152 | method: 'POST', 153 | headers: self.getHeader(true), 154 | body: JSON.stringify(user) 155 | }, (error, response, body) => { 156 | self.request(error, response, body, (error: OcsHttpError, body?) => { 157 | let userAdded = false; 158 | if (!error && body) { 159 | userAdded = true; 160 | } 161 | 162 | callback(error, userAdded); 163 | }); 164 | }); 165 | } 166 | 167 | export function ocsEditUser( 168 | userId: string, 169 | field: OcsEditUserField, 170 | value: string, 171 | callback: (error: OcsHttpError, result?: boolean) => void 172 | ): void { 173 | const self: OcsConnection = this; 174 | 175 | req({ 176 | url: `${self.options.url}/${baseUrl}/${userId}`, 177 | method: 'PUT', 178 | headers: self.getHeader(true), 179 | body: JSON.stringify({ value, key: field }) 180 | }, (error, response, body) => { 181 | self.request(error, response, body, (error: OcsHttpError, body?) => { 182 | let userEdited = false; 183 | if (!error && body) { 184 | userEdited = true; 185 | } 186 | 187 | callback(error, userEdited); 188 | }); 189 | }); 190 | } 191 | 192 | export function ocsGetUserGroups(userId: string, callback: (error: OcsHttpError, result?: string[]) => void): void { 193 | const self: OcsConnection = this; 194 | 195 | // Basic validation 196 | if (!userId) { 197 | callback({ code: 0, message: 'no userId specified' }); 198 | return; 199 | } 200 | 201 | req({ 202 | url: `${self.options.url}/${baseUrl}/${userId}/groups`, 203 | headers: self.getHeader() 204 | }, (error, response, body) => { 205 | self.request(error, response, body, (error: OcsHttpError, body?) => { 206 | let groups: string[] = null; 207 | 208 | if (!error && body && body.data && body.data.groups) { 209 | groups = []; 210 | body.data.groups.forEach(group => { 211 | groups.push(group); 212 | }); 213 | } 214 | 215 | callback(error, groups); 216 | }); 217 | }); 218 | } 219 | 220 | export function ocsAddRemoveUserForGroup( 221 | userId: string, 222 | groupId: string, 223 | toAdd: boolean, 224 | callback: (error: OcsHttpError, result?: boolean) => void 225 | ): void { 226 | const self: OcsConnection = this; 227 | 228 | // Basic validation 229 | if (!userId) { 230 | callback({ code: 0, message: 'no userId specified' }); 231 | return; 232 | } 233 | 234 | req({ 235 | url: `${self.options.url}/${baseUrl}/${userId}/groups`, 236 | method: (toAdd ? 'POST' : 'DELETE'), 237 | headers: self.getHeader(true), 238 | body: JSON.stringify({ groupid: groupId }) 239 | }, (error, response, body) => { 240 | self.request(error, response, body, (error: OcsHttpError, body?) => { 241 | let userModifiedForGroup = false; 242 | if (!error && body) { 243 | userModifiedForGroup = true; 244 | } 245 | 246 | callback(error, userModifiedForGroup); 247 | }); 248 | }); 249 | } 250 | 251 | export function ocsSetUserSubAdmin( 252 | userId: string, 253 | groupId: string, 254 | isSubAdmin: boolean, 255 | callback: (error: OcsHttpError, result?: boolean) => void 256 | ): void { 257 | const self: OcsConnection = this; 258 | 259 | // Basic validation 260 | if (!userId) { 261 | callback({ code: 0, message: 'no userId specified' }); 262 | return; 263 | } 264 | 265 | req({ 266 | url: `${self.options.url}/${baseUrl}/${userId}/subadmins`, 267 | method: (isSubAdmin ? 'POST' : 'DELETE'), 268 | headers: self.getHeader(true), 269 | body: JSON.stringify({ groupid: groupId }) 270 | }, (error, response, body) => { 271 | self.request(error, response, body, (error: OcsHttpError, body?) => { 272 | let subAdminModifiedForGroup = false; 273 | if (!error && body) { 274 | subAdminModifiedForGroup = true; 275 | } 276 | 277 | callback(error, subAdminModifiedForGroup); 278 | }); 279 | }); 280 | } 281 | 282 | export function ocsGetUserSubAdmins(userId: string, callback: (error: OcsHttpError, result?: string[]) => void): void { 283 | const self: OcsConnection = this; 284 | 285 | // Basic validation 286 | if (!userId) { 287 | callback({ code: 0, message: 'no userId specified' }); 288 | return; 289 | } 290 | 291 | req({ 292 | url: `${self.options.url}/${baseUrl}/${userId}/subadmins`, 293 | headers: self.getHeader() 294 | }, (error, response, body) => { 295 | self.request(error, response, body, (error: OcsHttpError, body?) => { 296 | let subAdmins: string[] = null; 297 | 298 | if (!error && body && body.data) { 299 | subAdmins = []; 300 | body.data.forEach(subAdmin => { 301 | subAdmins.push(subAdmin); 302 | }); 303 | } 304 | 305 | callback(error, subAdmins); 306 | }); 307 | }); 308 | } 309 | 310 | export function ocsResendUserWelcomeEmail(userId: string, callback: (error: OcsHttpError, result?: boolean) => void): void { 311 | const self: OcsConnection = this; 312 | 313 | // Basic validation 314 | if (!userId) { 315 | callback({ code: 0, message: 'no userId specified' }); 316 | return; 317 | } 318 | 319 | req({ 320 | url: `${self.options.url}/${baseUrl}/${userId}/welcome`, 321 | method: 'POST', 322 | headers: self.getHeader() 323 | }, (error, response, body) => { 324 | self.request(error, response, body, (error: OcsHttpError, body?) => { 325 | let success = false; 326 | if (!error && body) { 327 | success = true; 328 | } 329 | 330 | callback(error, success); 331 | }); 332 | }); 333 | } 334 | -------------------------------------------------------------------------------- /source/webdav.ts: -------------------------------------------------------------------------------- 1 | import { promisify } from 'util'; 2 | import * as Webdav from 'webdav-client'; 3 | import * as Stream from 'stream'; 4 | 5 | import { 6 | NextcloudClientInterface, 7 | FileDetailProperty, 8 | ConnectionOptions, 9 | FolderProperties, 10 | FileDetails, 11 | } from './types'; 12 | 13 | import { 14 | clientFunction 15 | } from './helper'; 16 | 17 | import { 18 | Exception as NextcloudError, 19 | 20 | NotReadyError 21 | } from './errors'; 22 | 23 | const sanitizePath = encodeURI; 24 | 25 | const promisifiedPut = promisify(Webdav.Connection.prototype.put); 26 | const promisifiedGet = promisify(Webdav.Connection.prototype.get); 27 | const promisifiedMove = promisify(Webdav.Connection.prototype.move); 28 | const promisifiedMkdir = promisify(Webdav.Connection.prototype.mkdir); 29 | const promisifiedExists = promisify(Webdav.Connection.prototype.exists); 30 | const promisifiedDelete = promisify(Webdav.Connection.prototype.delete); 31 | const promisifiedReaddir = promisify(Webdav.Connection.prototype.readdir); 32 | const promisifiedPreStream = promisify(Webdav.Connection.prototype.prepareForStreaming); 33 | const promisifiedGetProperties = promisify(Webdav.Connection.prototype.getProperties); 34 | 35 | async function rawGetReadStream(sanePath: string): Promise { 36 | const self: NextcloudClientInterface = this; 37 | 38 | await promisifiedPreStream.call(self.webdavConnection, sanePath); 39 | 40 | return await self.webdavConnection.get(sanePath); 41 | } 42 | 43 | async function rawRemove(sanePath: string): Promise { 44 | const self: NextcloudClientInterface = this; 45 | 46 | await promisifiedDelete.call(self.webdavConnection, sanePath); 47 | } 48 | 49 | async function rawExists(sanePath: string): Promise { 50 | const self: NextcloudClientInterface = this; 51 | 52 | const paths = unnest(sanePath); 53 | 54 | for (const sanePath of paths) { 55 | if (!await promisifiedExists.call(self.webdavConnection, sanePath)) { 56 | return false; 57 | } 58 | } 59 | 60 | return true; 61 | } 62 | 63 | async function rawPut(sanePath: string, content: Webdav.ContentType): Promise { 64 | const self: NextcloudClientInterface = this; 65 | 66 | await promisifiedPut.call(self.webdavConnection, sanePath, content); 67 | } 68 | 69 | async function rawGet(sanePath: string): Promise { 70 | const self: NextcloudClientInterface = this; 71 | 72 | return await promisifiedGet.call(self.webdavConnection, sanePath); 73 | } 74 | 75 | async function rawGetFiles(sanePath: string): Promise { 76 | const self: NextcloudClientInterface = this; 77 | 78 | const files: string[] = await promisifiedReaddir.call(self.webdavConnection, sanePath); 79 | 80 | if (!Array.isArray(files)) { 81 | throw new NotReadyError; 82 | } 83 | 84 | return files; 85 | } 86 | 87 | async function rawGetFolderFileDetails(sanePath: string, extraProperties?: FileDetailProperty[]): Promise { 88 | const self: NextcloudClientInterface = this; 89 | 90 | const options = { 91 | properties: true 92 | }; 93 | 94 | if (extraProperties && extraProperties.length > 0) { 95 | options['extraProperties'] = [...extraProperties]; 96 | } 97 | 98 | const files: FileDetails[] = await promisifiedReaddir.call(self.webdavConnection, sanePath, options); 99 | 100 | if (!Array.isArray(files)) { 101 | throw new NotReadyError; 102 | } 103 | 104 | return files; 105 | } 106 | 107 | async function rawGetFolderProperties(sanePath: string, extraProperties?: FileDetailProperty[]): Promise { 108 | const self: NextcloudClientInterface = this; 109 | 110 | const options = { 111 | properties: true 112 | }; 113 | 114 | if (extraProperties && extraProperties.length > 0) { 115 | options['extraProperties'] = [...extraProperties]; 116 | } 117 | 118 | const result = await promisifiedGetProperties.call(self.webdavConnection, sanePath, options); 119 | 120 | return result; 121 | } 122 | 123 | async function rawRename(saneFrom: string, newName: string): Promise { 124 | const self: NextcloudClientInterface = this; 125 | 126 | const override = true; 127 | const base = saneFrom.slice(0, saneFrom.lastIndexOf('/') + 1); 128 | 129 | const fullDestinationPath = `${nextcloudRoot(self.url, self.username)}${base}${sanitizePath(newName)}`; 130 | 131 | await promisifiedMove.call(self.webdavConnection, saneFrom, fullDestinationPath, override); 132 | } 133 | 134 | async function rawMove(saneFrom: string, toPath: string): Promise { 135 | const self: NextcloudClientInterface = this; 136 | 137 | const fullDestinationPath = `${nextcloudRoot(self.url, self.username)}${sanitizePath(toPath)}`; 138 | const override = true; 139 | 140 | await promisifiedMove.call(self.webdavConnection, saneFrom, fullDestinationPath, override); 141 | } 142 | 143 | async function rawGetWriteStream(sanePath: string): Promise { 144 | const self: NextcloudClientInterface = this; 145 | 146 | await preWriteStream.call(self, sanePath); 147 | 148 | return await self.webdavConnection.put(sanePath); 149 | } 150 | 151 | async function rawTouchFolder(sanePath: string): Promise { 152 | const self: NextcloudClientInterface = this; 153 | 154 | if (!await rawExists.call(self, sanePath)) { 155 | await promisifiedMkdir.call(self.webdavConnection, sanePath); 156 | } 157 | } 158 | 159 | async function rawCreateFolderHierarchy(sanePath: string): Promise { 160 | const self: NextcloudClientInterface = this; 161 | 162 | const paths = unnest(sanePath); 163 | 164 | for (const saneSubfolder of paths) { 165 | await rawTouchFolder.call(self, saneSubfolder); 166 | } 167 | } 168 | 169 | export function configureWebdavConnection(options: ConnectionOptions): void { 170 | const self: NextcloudClientInterface = this; 171 | 172 | self.webdavConnection = new Webdav.Connection({ 173 | url: nextcloudRoot(options.url, options.username), 174 | authenticator: new Webdav.BasicAuthenticator(), 175 | username: options.username, 176 | password: options.password 177 | }); 178 | } 179 | 180 | export async function checkConnectivity(): Promise { 181 | const self: NextcloudClientInterface = this; 182 | 183 | try { await rawGetFiles.call(self, '/'); } 184 | catch (error) { return false; } 185 | 186 | return true; 187 | } 188 | 189 | async function rawPipeStream(saneTargetPath: string, readStream: Stream.Readable): Promise { 190 | process.emitWarning('pipeStream has been deprecated and will be removed in version 2, use uploadFromStream instead.'); 191 | 192 | const self: NextcloudClientInterface = this; 193 | 194 | const writeStream = await rawGetWriteStream.call(self, saneTargetPath); 195 | 196 | await pipeStreams(readStream, writeStream); 197 | } 198 | 199 | async function rawUploadFromStream(saneTargetPath: string, readStream: Stream.Readable): Promise { 200 | const self: NextcloudClientInterface = this; 201 | 202 | const writeStream = await rawGetWriteStream.call(self, saneTargetPath); 203 | 204 | await pipeStreams(readStream, writeStream); 205 | } 206 | 207 | async function rawDownloadToStream(saneSourcePath: string, writeStream: Stream.Writable): Promise { 208 | const self: NextcloudClientInterface = this; 209 | 210 | const readStream = await rawGetReadStream.call(self, saneSourcePath); 211 | 212 | await pipeStreams(readStream, writeStream); 213 | } 214 | 215 | export const createFolderHierarchy = clientFunction(rawCreateFolderHierarchy); 216 | export const getFolderFileDetails = clientFunction(rawGetFolderFileDetails); 217 | export const getFolderProperties = clientFunction(rawGetFolderProperties); 218 | export const getWriteStream = clientFunction(rawGetWriteStream); 219 | export const getReadStream = clientFunction(rawGetReadStream); 220 | export const touchFolder = clientFunction(rawTouchFolder); 221 | export const pipeStream = clientFunction(rawPipeStream); // deprecated 222 | export const uploadFromStream = clientFunction(rawUploadFromStream); 223 | export const downloadToStream = clientFunction(rawDownloadToStream); 224 | export const getFiles = clientFunction(rawGetFiles); 225 | export const rename = clientFunction(rawRename); 226 | export const remove = clientFunction(rawRemove); 227 | export const move = clientFunction(rawMove); 228 | export const exists = clientFunction(rawExists); 229 | export const put = clientFunction(rawPut); 230 | export const get = clientFunction(rawGet); 231 | 232 | async function preWriteStream(sanitizedPath: string): Promise { 233 | const self: NextcloudClientInterface = this; 234 | 235 | await promisifiedPut.call(self.webdavConnection, sanitizedPath, ''); 236 | 237 | await promisifiedPreStream.call(self.webdavConnection, sanitizedPath); 238 | } 239 | 240 | function unnest(path) { 241 | return path 242 | .slice(1) 243 | .split('/') 244 | .map((folder, position, folders) => `/${folders.slice(0, position + 1).join('/')}`); 245 | } 246 | 247 | function nextcloudRoot(url, username) { 248 | const lastUrlCharacterIsSlash = url.slice(-1)[0] === '/'; 249 | 250 | const terminatedUrl = lastUrlCharacterIsSlash ? url : `${url}/`; 251 | 252 | return `${terminatedUrl}remote.php/dav/files/${username}`; 253 | } 254 | 255 | async function pipeStreams(readStream: Stream.Readable, writeStream: Stream.Writable): Promise { 256 | return new Promise((resolve, reject) => { 257 | readStream.on('error', wrapError); 258 | writeStream.on('error', wrapError); 259 | 260 | // event from WebDav.Stream's deprecated request in case of uploadFromStream 261 | writeStream.on('end', resolve); 262 | 263 | // event from Node.js write stream in case of downloadToStream 264 | writeStream.on('close', resolve); 265 | 266 | readStream.pipe(writeStream); 267 | 268 | function wrapError(error) { 269 | reject(NextcloudError(error)); 270 | } 271 | }); 272 | } 273 | -------------------------------------------------------------------------------- /tests/groupfolders-jest.ts: -------------------------------------------------------------------------------- 1 | import NextcloudClient from '../source/client'; 2 | import configuration from './configuration'; 3 | import { execSync } from 'child_process'; 4 | 5 | describe('Groupfolders integration', function testGroupfoldersIntegration() { 6 | const client = new NextcloudClient(configuration.connectionOptions); 7 | 8 | beforeAll(() => { 9 | execSync(`docker exec -u 33 nextcloud-link_nextcloud_1 bash -c 'php occ app:install groupfolders'`); 10 | execSync(`docker exec -u 33 nextcloud-link_nextcloud_1 bash -c 'php occ app:enable groupfolders'`); 11 | }); 12 | 13 | describe('getFolders() and getFolder(fid)', () => { 14 | afterAll(() => { 15 | execSync(`docker exec -u 33 nextcloud-link_nextcloud_1 bash -c 'php occ groupfolders:delete 1 -f'`); 16 | execSync(`docker exec -u 33 nextcloud-link_nextcloud_1 bash -c 'php occ groupfolders:delete 2 -f'`); 17 | }) 18 | 19 | it('should return an empty array if there are no groupfolders', async () => { 20 | expect(await client.groupfolders.getFolders()).toEqual([]); 21 | }); 22 | 23 | it('should return an array with existing groupfolder', async () => { 24 | execSync(`docker exec -u 33 nextcloud-link_nextcloud_1 bash -c 'php occ groupfolders:create testing'`); 25 | 26 | const groupfolders = await client.groupfolders.getFolders(); 27 | 28 | expect(groupfolders).toEqual([ 29 | { 30 | acl: false, 31 | groups: [], 32 | id: 1, 33 | manage: [], 34 | mountPoint: 'testing', 35 | quota: -3, 36 | size: 0, 37 | } 38 | ]); 39 | }); 40 | 41 | it('should return an array with existing groupfolders', async () => { 42 | execSync(`docker exec -u 33 nextcloud-link_nextcloud_1 bash -c 'php occ groupfolders:create another'`); 43 | 44 | const groupfolders = await client.groupfolders.getFolders(); 45 | 46 | expect(groupfolders).toEqual([ 47 | { 48 | acl: false, 49 | groups: [], 50 | id: 1, 51 | manage: [], 52 | mountPoint: 'testing', 53 | quota: -3, 54 | size: 0, 55 | }, 56 | { 57 | id: 2, 58 | mountPoint: 'another', 59 | groups: [], 60 | quota: -3, 61 | size: 0, 62 | acl: false, 63 | manage: [] 64 | }, 65 | ]); 66 | }); 67 | 68 | it('should return existing groupfolder', async () => { 69 | const groupfolder = await client.groupfolders.getFolder(1); 70 | 71 | expect(groupfolder).toEqual({ 72 | acl: false, 73 | groups: [], 74 | id: 1, 75 | // manage: [], // known issue https://github.com/nextcloud/groupfolders/issues/885 76 | mountPoint: 'testing', 77 | quota: -3, 78 | size: 0, 79 | }); 80 | }); 81 | 82 | it('should return null if the requested groupfolder does not exist', async () => { 83 | const groupfolder = await client.groupfolders.getFolder(999); 84 | 85 | expect(groupfolder).toEqual(null); 86 | }); 87 | }); 88 | 89 | describe('addFolder(mountpoint)', () => { 90 | it('should add new groupfolder and return its id', async () => { 91 | const mountpoint = 'some name'; 92 | const groupfolderId = await client.groupfolders.addFolder(mountpoint); 93 | 94 | expect(groupfolderId).toBe(3); 95 | }); 96 | }); 97 | 98 | describe('removeFolder(fid)', () => { 99 | it('should remove existing groupfolder and return true', async () => { 100 | expect(await client.groupfolders.getFolder(3)).toBeDefined(); 101 | 102 | expect(await client.groupfolders.removeFolder(3)).toBe(true); 103 | 104 | expect(await client.groupfolders.getFolder(3)).toBe(null); 105 | }); 106 | 107 | it('should return true even if the groupfolder does not exist', async () => { 108 | expect(await client.groupfolders.getFolder(999)).toBe(null); 109 | 110 | expect(await client.groupfolders.removeFolder(999)).toBe(true); 111 | }); 112 | }); 113 | 114 | describe('addGroup(fid, gid), removeGroup(fid, gid), setPermissions(fid, gid, permissions)', () => { 115 | let groupfolderId; 116 | const group = 'admin'; 117 | 118 | beforeAll(async () => { 119 | execSync(`docker exec -u 33 nextcloud-link_nextcloud_1 bash -c 'php occ groupfolders:create testing'`); 120 | 121 | groupfolderId = (await client.groupfolders.getFolders())?.[0]?.id; 122 | }) 123 | 124 | it('should add group to existing groupfolder', async () => { 125 | const groupfolder = await client.groupfolders.getFolder(groupfolderId); 126 | expect(groupfolder.groups).toEqual([]); 127 | 128 | expect(await client.groupfolders.addGroup(groupfolderId, group)).toBe(true); 129 | 130 | const groupfolderAfter = await client.groupfolders.getFolder(groupfolderId); 131 | expect(groupfolderAfter.groups).toEqual({ [group]: 31 }); 132 | }); 133 | 134 | it('should throw an error when the same group is added repeatedly', async () => { 135 | await expect(client.groupfolders.addGroup(groupfolderId, group)).rejects.toBeDefined(); 136 | }); 137 | 138 | it('should not throw when addign group to non-existing groupfolder', async () => { 139 | expect(await client.groupfolders.addGroup(groupfolderId + 100, group)).toBe(true); 140 | }); 141 | 142 | it('should set group permissions on existing groupfolder', async () => { 143 | expect(await client.groupfolders.setPermissions(groupfolderId, group, 1)).toBe(true); 144 | 145 | expect(await client.groupfolders.getFolder(groupfolderId)).toMatchObject({ groups: { [group]: 1 } }); 146 | 147 | expect(await client.groupfolders.setPermissions(groupfolderId, group, 0)).toBe(true); 148 | 149 | expect(await client.groupfolders.getFolder(groupfolderId)).toMatchObject({ groups: { [group]: 0 } }); 150 | }); 151 | 152 | it('should not throw when setting group permissions on non-existing groupfolder', async () => { 153 | expect(await client.groupfolders.setPermissions(groupfolderId + 100, group, 1)).toBe(true); 154 | }); 155 | 156 | it('should remove group from existing groupfolder', async () => { 157 | expect(await client.groupfolders.removeGroup(groupfolderId, group)).toBe(true); 158 | 159 | const groupfolderAfter = await client.groupfolders.getFolder(groupfolderId); 160 | expect(groupfolderAfter.groups).toEqual([]); 161 | }); 162 | }); 163 | 164 | describe('enableACL(fid, enable)', () => { 165 | let groupfolderId; 166 | 167 | beforeAll(async () => { 168 | groupfolderId = (await client.groupfolders.getFolders())?.[0]?.id; 169 | }) 170 | 171 | it('should enable ACL on existing groupfolder', async () => { 172 | expect(await client.groupfolders.enableACL(groupfolderId, true)).toBe(true); 173 | 174 | expect(await client.groupfolders.getFolder(groupfolderId)).toMatchObject({ acl: true }); 175 | }); 176 | 177 | it('should disable ACL on existing groupfolder', async () => { 178 | expect(await client.groupfolders.enableACL(groupfolderId, false)).toBe(true); 179 | 180 | expect(await client.groupfolders.getFolder(groupfolderId)).toMatchObject({ acl: false }); 181 | }); 182 | 183 | it('should not throw when enabling ACL on non-existing groupfolder', async () => { 184 | expect(await client.groupfolders.enableACL(groupfolderId + 100, true)).toBe(true); 185 | }); 186 | }); 187 | 188 | describe('setManageACL(fid, type, id, manageACL)', () => { 189 | let groupfolderId; 190 | 191 | beforeAll(async () => { 192 | groupfolderId = (await client.groupfolders.getFolders())?.[0]?.id; 193 | }) 194 | 195 | it('should enable managing ACL for a "admin" group on existing groupfolder', async () => { 196 | expect((await client.groupfolders.getFolders())?.[0]).toMatchObject({ manage: [] }); 197 | 198 | expect(await client.groupfolders.setManageACL(groupfolderId, 'group', 'admin', true)).toBe(true); 199 | 200 | expect((await client.groupfolders.getFolders())?.[0]).toMatchObject({ manage: [{ 201 | displayname: 'admin', 202 | id: 'admin', 203 | type: 'group', 204 | }] }); 205 | }); 206 | 207 | it('should disable managing ACL for a "admin" group on existing groupfolder', async () => { 208 | expect(await client.groupfolders.setManageACL(groupfolderId, 'group', 'admin', false)).toBe(true); 209 | 210 | expect((await client.groupfolders.getFolders())?.[0]).toMatchObject({ manage: [] }); 211 | }); 212 | 213 | it('should only enable managing ACL for existing users', async () => { 214 | expect((await client.groupfolders.getFolders())?.[0]).toMatchObject({ manage: [] }); 215 | 216 | expect(await client.groupfolders.setManageACL(groupfolderId, 'user', 'nextcloud', true)).toBe(true); 217 | 218 | expect((await client.groupfolders.getFolders())?.[0]).toMatchObject({ manage: [{ 219 | displayname: 'nextcloud', 220 | id: 'nextcloud', 221 | type: 'user', 222 | }] }); 223 | }); 224 | }); 225 | 226 | describe('setQuota(fid, quota)', () => { 227 | let groupfolderId; 228 | 229 | beforeAll(async () => { 230 | groupfolderId = (await client.groupfolders.getFolders())?.[0]?.id; 231 | }) 232 | 233 | it('should set quota on existing groupfolder', async () => { 234 | expect(await client.groupfolders.setQuota(groupfolderId, 1000)).toBe(true); 235 | 236 | expect(await client.groupfolders.getFolder(groupfolderId)).toMatchObject({ quota: 1000 }); 237 | }); 238 | 239 | it('should set quota on existing groupfolder', async () => { 240 | expect(await client.groupfolders.setQuota(groupfolderId, -3)).toBe(true); 241 | 242 | expect(await client.groupfolders.getFolder(groupfolderId)).toMatchObject({ quota: -3 }); 243 | }); 244 | 245 | it('should not throw when setting quota on non-existing groupfolder', async () => { 246 | expect(await client.groupfolders.setQuota(groupfolderId + 100, 1000)).toBe(true); 247 | }); 248 | }); 249 | 250 | describe('renameFolder(fid, mountpoint)', () => { 251 | let groupfolderId; 252 | 253 | beforeAll(async () => { 254 | groupfolderId = (await client.groupfolders.getFolders())?.[0]?.id; 255 | }) 256 | 257 | it('should rename existing groupfolder', async () => { 258 | expect(await client.groupfolders.renameFolder(groupfolderId, 'new name')).toBe(true); 259 | 260 | expect(await client.groupfolders.getFolder(groupfolderId)).toMatchObject({ mountPoint: 'new name' }); 261 | }); 262 | 263 | it('should not throw when renaming non-existing groupfolder', async () => { 264 | expect(await client.groupfolders.renameFolder(groupfolderId + 100, 'new name')).toBe(true); 265 | }); 266 | }); 267 | }); 268 | -------------------------------------------------------------------------------- /source/ocs/groupfolders.ts: -------------------------------------------------------------------------------- 1 | import req from 'request'; 2 | 3 | import { 4 | OcsHttpError, 5 | OcsGroupfolder, 6 | } from './types'; 7 | 8 | import { OcsConnection } from './ocs-connection'; 9 | 10 | const baseUrl = 'apps/groupfolders/folders'; 11 | 12 | // GET apps/groupfolders/folders: Returns a list of all configured groupfolders and their settings 13 | export function ocsGetGroupfolders( 14 | callback: (error: OcsHttpError, result?: OcsGroupfolder[]) => void 15 | ): void { 16 | const self: OcsConnection = this; 17 | 18 | req({ 19 | url: `${self.options.url}/${baseUrl}`, 20 | headers: self.getHeader(true), 21 | }, (error, response, body) => { 22 | self.request(error, response, body, (error: OcsHttpError, body?) => { 23 | let result: OcsGroupfolder[] = null; 24 | 25 | if (!error && body && body.data) { 26 | result = []; 27 | 28 | Object.values(body.data).forEach(groupfolder => { 29 | result.push(parseOcsGroupfolder(groupfolder)); 30 | }); 31 | } 32 | 33 | callback(error, result); 34 | }); 35 | }); 36 | } 37 | 38 | // GET apps/groupfolders/folders/$folderId: Return a specific configured groupfolder and its settings 39 | // returns groupfolder object if found, `null` otherwise 40 | export function ocsGetGroupfolder( 41 | groupfolderId: number, 42 | callback: (error: OcsHttpError, result?: OcsGroupfolder) => void 43 | ): void { 44 | const self: OcsConnection = this; 45 | 46 | req({ 47 | url: `${self.options.url}/${baseUrl}/${groupfolderId}`, 48 | headers: self.getHeader(true), 49 | }, (error, response, body) => { 50 | self.request(error, response, body, (error: OcsHttpError, body?) => { 51 | let result: OcsGroupfolder = null; 52 | 53 | if (!error && body && body.data) { 54 | result = parseOcsGroupfolder(body.data); 55 | } 56 | 57 | callback(error, result); 58 | }); 59 | }); 60 | } 61 | 62 | // POST apps/groupfolders/folders: Create a new groupfolder 63 | // `mountpoint`: The name for the new groupfolder 64 | // returns new groupfolder id 65 | export function ocsAddGroupfolder( 66 | mountpoint: string, 67 | callback: (error: OcsHttpError, result?: number) => void 68 | ): void { 69 | const self: OcsConnection = this; 70 | 71 | const body = { 72 | mountpoint, 73 | }; 74 | 75 | req({ 76 | url: `${self.options.url}/${baseUrl}`, 77 | method: 'POST', 78 | headers: self.getHeader(true), //! set `true` for POST requests 79 | body: JSON.stringify(body), 80 | }, (error, response, body) => { 81 | self.request(error, response, body, (error: OcsHttpError, body?) => { 82 | let result: number = null; 83 | 84 | if (!error && body && body.data) { 85 | result = parseOcsGroupfolderId(body.data); 86 | } 87 | 88 | callback(error, result); 89 | }); 90 | }); 91 | } 92 | 93 | // DELETE apps/groupfolders/folders/$folderId: Delete a groupfolder 94 | // returns `true` if successful (even if the groupfolder didn't exist) 95 | export function ocsRemoveGroupfolder( 96 | groupfolderId: number, 97 | callback: (error: OcsHttpError, result?: boolean) => void 98 | ): void { 99 | const self: OcsConnection = this; 100 | 101 | req({ 102 | url: `${self.options.url}/${baseUrl}/${groupfolderId}`, 103 | method: 'DELETE', 104 | headers: self.getHeader(), 105 | }, (error, response, body) => { 106 | self.request(error, response, body, (error: OcsHttpError, body?) => { 107 | let groupfolderDeleted = false; 108 | 109 | if (!error && body) { 110 | groupfolderDeleted = true; 111 | } 112 | 113 | callback(error, groupfolderDeleted); 114 | }); 115 | }); 116 | } 117 | 118 | // POST apps/groupfolders/folders/$folderId/groups: Give a group access to a groupfolder 119 | // `group`: The id of the group to be given access to the groupfolder 120 | // returns `true` if successful (even if the group doesn't exist) 121 | export function ocsAddGroupfolderGroup( 122 | groupfolderId: number, 123 | groupId: string, 124 | callback: (error: OcsHttpError, result?: boolean) => void 125 | ): void { 126 | const self: OcsConnection = this; 127 | 128 | const body = { 129 | group: groupId, 130 | }; 131 | 132 | req({ 133 | url: `${self.options.url}/${baseUrl}/${groupfolderId}/groups`, 134 | method: 'POST', 135 | headers: self.getHeader(true), 136 | body: JSON.stringify(body) 137 | }, (error, response, body) => { 138 | self.request(error, response, body, (error: OcsHttpError, body?) => { 139 | let groupfolderGroupAdded = false; 140 | 141 | if (!error && body) { 142 | groupfolderGroupAdded = true; 143 | } 144 | 145 | callback(error, groupfolderGroupAdded); 146 | }); 147 | }); 148 | } 149 | 150 | // DELETE apps/groupfolders/folders/$folderId/groups/$groupId: Remove access from a group to a groupfolder 151 | // returns `true` if successful (even if the groupfolder didn't exist) 152 | export function ocsRemoveGroupfolderGroup( 153 | groupfolderId: number, 154 | groupId: string, 155 | callback: (error: OcsHttpError, result?: boolean) => void 156 | ): void { 157 | const self: OcsConnection = this; 158 | 159 | req({ 160 | url: `${self.options.url}/${baseUrl}/${groupfolderId}/groups/${groupId}`, 161 | method: 'DELETE', 162 | headers: self.getHeader(), 163 | }, (error, response, body) => { 164 | self.request(error, response, body, (error: OcsHttpError, body?) => { 165 | let groupfolderGroupRemoved = false; 166 | 167 | if (!error && body) { 168 | groupfolderGroupRemoved = true; 169 | } 170 | 171 | callback(error, groupfolderGroupRemoved); 172 | }); 173 | }); 174 | } 175 | 176 | // POST apps/groupfolders/folders/$folderId/groups/$groupId: Set the permissions a group has in a groupfolder 177 | // `permissions` The new permissions for the group as bitmask of permissions constants 178 | // e.g. write(6) === update(2) + create(4) 179 | export function ocsSetGroupfolderPermissions( 180 | groupfolderId: number, 181 | groupId: string, 182 | permissions: number, 183 | callback: (error: OcsHttpError, result?: boolean) => void 184 | ): void { 185 | const self: OcsConnection = this; 186 | 187 | const body = { 188 | permissions, 189 | }; 190 | 191 | req({ 192 | url: `${self.options.url}/${baseUrl}/${groupfolderId}/groups/${groupId}`, 193 | method: 'POST', 194 | headers: self.getHeader(true), 195 | body: JSON.stringify(body), 196 | }, (error, response, body) => { 197 | self.request(error, response, body, (error: OcsHttpError, body?) => { 198 | let groupfolderPermissionsSet = false; 199 | 200 | if (!error && body) { 201 | groupfolderPermissionsSet = true; 202 | } 203 | 204 | callback(error, groupfolderPermissionsSet); 205 | }); 206 | }); 207 | } 208 | 209 | // POST apps/groupfolders/folders/$folderId/acl: Enable/Disable groupfolder advanced permissions 210 | // `acl`: `true` for enable, `false` for disable. 211 | export function ocsEnableOrDisableGroupfolderACL( 212 | groupfolderId: number, 213 | enable: boolean, 214 | callback: (error: OcsHttpError, result?: boolean) => void 215 | ): void { 216 | const self: OcsConnection = this; 217 | 218 | const body = { 219 | acl: enable ? 1 : 0 220 | }; 221 | 222 | req({ 223 | url: `${self.options.url}/${baseUrl}/${groupfolderId}/acl`, 224 | method: 'POST', 225 | headers: self.getHeader(true), 226 | body: JSON.stringify(body), 227 | }, (error, response, body) => { 228 | self.request(error, response, body, (error: OcsHttpError, body?) => { 229 | let groupfolderACLset = false; 230 | 231 | if (!error && body) { 232 | groupfolderACLset = true; 233 | } 234 | 235 | callback(error, groupfolderACLset); 236 | }); 237 | }); 238 | } 239 | 240 | // POST apps/groupfolders/folders/$folderId/manageACL: Grants/Removes a group or user the ability to manage a groupfolders' advanced permissions 241 | // `mappingId`: the id of the group/user to be granted/removed access to/from the groupfolder 242 | // `mappingType`: 'group' or 'user' 243 | // `manageAcl`: true to grants ability to manage a groupfolders' advanced permissions, false to remove 244 | export function ocsSetGroupfolderManageACL( 245 | groupfolderId: number, 246 | type: 'group' | 'user', 247 | id: string, 248 | manageACL: boolean, 249 | callback: (error: OcsHttpError, result?: boolean) => void 250 | ): void { 251 | const self: OcsConnection = this; 252 | 253 | const body = { 254 | mappingType: type, 255 | mappingId: id, 256 | manageAcl: manageACL ? 1 : 0 257 | }; 258 | 259 | req({ 260 | url: `${self.options.url}/${baseUrl}/${groupfolderId}/manageACL`, 261 | method: 'POST', 262 | headers: self.getHeader(true), 263 | body: JSON.stringify(body), 264 | }, (error, response, body) => { 265 | self.request(error, response, body, (error: OcsHttpError, body?) => { 266 | let groupfolderPermissionsSet = false; 267 | 268 | if (!error && body) { 269 | groupfolderPermissionsSet = true; 270 | } 271 | 272 | callback(error, groupfolderPermissionsSet); 273 | }); 274 | }); 275 | } 276 | 277 | // POST apps/groupfolders/folders/$folderId/quota: Set the quota for a groupfolder in bytes 278 | // `quota`: The new quota for the groupfolder in bytes, user -3 for unlimited 279 | export function ocsSetGroupfolderQuota( 280 | groupfolderId: number, 281 | quota: number, 282 | callback: (error: OcsHttpError, result?: boolean) => void 283 | ): void { 284 | const self: OcsConnection = this; 285 | 286 | const body = { 287 | quota: Number.isNaN(quota) ? -3 : quota, 288 | }; 289 | 290 | req({ 291 | url: `${self.options.url}/${baseUrl}/${groupfolderId}/quota`, 292 | method: 'POST', 293 | headers: self.getHeader(true), 294 | body: JSON.stringify(body), 295 | }, (error, response, body) => { 296 | self.request(error, response, body, (error: OcsHttpError, body?) => { 297 | let groupfolderQuotaSet = false; 298 | 299 | if (!error && body) { 300 | groupfolderQuotaSet = true; 301 | } 302 | 303 | callback(error, groupfolderQuotaSet); 304 | }); 305 | }); 306 | } 307 | 308 | // POST apps/groupfolders/folders/$folderId/mountpoint: Change the name of a groupfolder 309 | // `mountpoint`: The new name for the groupfolder 310 | export function ocsRenameGroupfolder( 311 | groupfolderId: number, 312 | mountpoint: string, 313 | callback: (error: OcsHttpError, result?: boolean) => void 314 | ): void { 315 | const self: OcsConnection = this; 316 | 317 | const body = { 318 | mountpoint, 319 | }; 320 | 321 | req({ 322 | url: `${self.options.url}/${baseUrl}/${groupfolderId}/mountpoint`, 323 | method: 'POST', 324 | headers: self.getHeader(true), 325 | body: JSON.stringify(body), 326 | }, (error, response, body) => { 327 | self.request(error, response, body, (error: OcsHttpError, body?) => { 328 | let groupfolderRenamed = false; 329 | 330 | if (!error && body) { 331 | groupfolderRenamed = true; 332 | } 333 | 334 | callback(error, groupfolderRenamed); 335 | }); 336 | }); 337 | } 338 | 339 | function parseOcsGroupfolder(groupfolder): OcsGroupfolder { 340 | return { 341 | id: parseInt(groupfolder.id, 10), 342 | mountPoint: groupfolder.mount_point, 343 | groups: groupfolder.groups, 344 | quota: groupfolder.quota, 345 | size: groupfolder.size, 346 | acl: groupfolder.acl, 347 | manage: groupfolder.manage, 348 | }; 349 | } 350 | 351 | function parseOcsGroupfolderId(groupfolder): number { 352 | return parseInt(groupfolder.id, 10); 353 | } 354 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nextcloud-link ![npm](https://img.shields.io/npm/v/nextcloud-link?label=version) 2 | 3 | ![](https://github.com/tentwentyfour/nextcloud-link/workflows/Node.js%20CI/badge.svg) 4 | ![Snyk Vulnerabilities for GitHub Repo](https://img.shields.io/snyk/vulnerabilities/github/tentwentyfour/nextcloud-link) 5 | [![NPM Downloads](https://img.shields.io/npm/dt/nextcloud-link.svg?style=flat)](https://npmjs.org/package/nextcloud-link) 6 | [![Greenkeeper badge](https://badges.greenkeeper.io/tentwentyfour/nextcloud-link.svg)](https://greenkeeper.io/) 7 | ![GitHub](https://img.shields.io/github/license/tentwentyfour/nextcloud-link?color=blue) 8 | ![Twitter Follow](https://img.shields.io/twitter/follow/1024Lu?label=Follow%20TenTwentyFour&style=social) 9 | 10 | > Node.js client to interact with [Nextcloud](https://nextcloud.com), developed with :hearts: by [TenTwentyFour](https://tentwentyfour.lu). 11 | 12 | ![tiny persons handling files in a huge directory](./cloud.png "Directory") 13 | 14 | ## Table of Contents 15 | 16 | - [Getting Started](#getting-started) 17 | - [Features](#features) 18 | - [Interface](#interface) 19 | - [Core](#core) 20 | - [Activities](#activities) 21 | - [Users](#users) 22 | - [Groups](#groups) 23 | - [Shares](#shares) 24 | - [Groupfolders](#groupfolders) 25 | - [Exceptions](#exceptions) 26 | - [Types](#types) 27 | - [Helpers](#helpers) 28 | - [Definitions](#definitions) 29 | - [Contributing](#contributing) 30 | 31 | ## Getting started 32 | 33 | If you're not planning on contributing code to the project, you can simply install `nextcloud-link` to your project by running: 34 | 35 | `npm install --save nextcloud-link` 36 | 37 | ### Quick-Start 38 | 39 | Establishing a connection from your ECMA- or TypeScript project to a Nextcloud instance can be done like this: 40 | 41 | ```TypeScript 42 | import NextcloudClient from 'nextcloud-link'; 43 | 44 | const client = new NextcloudClient({ 45 | "url": "https://my.nextcloud.com", 46 | "password": "useSomeBetterPassphraseThanThis", 47 | "username": "cloudrider", 48 | }); 49 | ``` 50 | 51 | Once you have initiated the connection to your nextcloud instance, it is generally a good idea to delay any file or OCS operations until the connection to your instance has been established and verified. Using the `client` object from above, we can do this like so: 52 | 53 | ```TypeScript 54 | while (true) { 55 | if (await client.checkConnectivity()) { 56 | return; 57 | } 58 | 59 | await new Promise(resolve => setTimeout(resolve, 5000)); 60 | } 61 | ``` 62 | 63 | In a real set-up, you'll probably want to limit the number of tries to something sensible, like 15 to 30 seconds by throwing after a given number of attempts. 64 | 65 | Finally, use any of the methods described below to interact with your Nextcloud instance: 66 | 67 | ```TypeScript 68 | const uploader = await client.getCreatorByPath('/Nextcloud.png'); 69 | ``` 70 | 71 | ## Features 72 | 73 | - :link: Interacts with Nextcloud instances via the WebDAV protocol 74 | - :rocket: Allows the use of streams for file transfer 75 | - :pray: Asserts Nextcloud connectivity before attempting any requests 76 | - :tada: OCS methods for groups, users, shares, activity, and groupfolders 77 | 78 | ## Interface 79 | 80 | ### Core 81 | 82 | The following methods are available on `client`: 83 | 84 | `configureWebdavConnection(options: ConnectionOptions): void` 85 | > Configures the Nextcloud connection to talk to a specific Nextcloud WebDav endpoint. This does not issue any kind of request, so it doesn't throw if the parameters are incorrect. This merely sets internal variables. 86 | 87 | `checkConnectivity(): Promise` 88 | > Checks whether the connection to the configured WebDav endpoint succeeds. This does not throw, it consistently returns a Promise wrapping a boolean. 89 | 90 | `pipeStream(path: string, stream: Stream.Readable): Promise` 91 | > Deprecated, will be removed in version 2, use uploadFromStream 92 | 93 | `uploadFromStream(targetPath: string, stream: Stream.Readable): Promise` 94 | > Saves the data obtained through `stream` to the Nextcloud instance at `path`. Throws a `NotFoundError` if the requested path does not exist. 95 | 96 | `downloadToStream(sourcePath: string, stream: Stream.Readable): Promise` 97 | > Pipes the data obtained by reading a file at `path` on the Nextcloud instance to the provided local `stream`. Throws a `NotFoundError` if the requested path does not exist. 98 | 99 | `as(username: string, password: string): NextcloudClient` 100 | > Creates a copy of the client that runs the request as the user with the passed credentials. This does absolutely no verification, so you should use `checkConnectivity` to verify the credentials. 101 | 102 | `createFolderHierarchy(path: string): Promise` 103 | > This is basically a recursive `mkdir`. 104 | 105 | `put(path: string, content: Webdav.ContentType): Promise` 106 | > This saves a Webdav.ContentType at `path`. Throws a `NotFoundError` if the path to the requested directory does not exist. 107 | 108 | `rename(fromFullPath: string, toFileName: string): Promise` 109 | > This allows to rename files or directories. 110 | 111 | `move(fromFullPath: string, toFullPath: string): Promise` 112 | > This allows to move files or entire directories. 113 | 114 | `getWriteStream(path: string): Promise` 115 | > Gets a write stream to a remote Nextcloud `path`. Throws a `NotFoundError` if the path to the requested directory does not exist. 116 | 117 | `getReadStream(path: string): Promise` 118 | > Gets a read stream to a remote Nextcloud `path`. 119 | 120 | `getFolderProperties(path: string, extraProperties?: FileDetailProperty[]): Promise` 121 | > Retrieves properties for the folder. Use [extraProperties](#webdav-extraproperties) to request properties not returned by default. 122 | 123 | `touchFolder(path: string): Promise` 124 | > Smart `mkdir` implementation that doesn't complain if the folder at `path` already exists. 125 | 126 | `getFiles(path: string): Promise` 127 | > List files in a directory. 128 | 129 | `getFolderFileDetails(path: string, extraProperties?: FileDetailProperty[]): Promise` 130 | > Same as `getFiles`, but returns more details instead of just file names. Use extraProperties to request properties not returned by default. 131 | 132 | `remove(path: string): Promise` 133 | > Removes file or directories. Does not complain if directories aren't empty. 134 | 135 | `exists(path: string): Promise` 136 | > Simple test that checks whether a file or directory exists. This indicates it in the return value, not by throwing exceptions. 137 | 138 | `get(path: string): Promise` 139 | > Gets a file as a string/Buffer. 140 | 141 | `getCreatorByPath(path: string): Promise` 142 | > Gets the username of the user that created the file or folder. 143 | 144 | `getCreatorByFileId(fileId: number|string): Promise` 145 | > Gets the username of the user that created the file or folder. 146 | 147 | ### Activities 148 | The following methods are available on `client.activities` 149 | 150 | `get(fileId: number|string, sort?: 'asc'|'desc', limit?: number, sinceActivityId?: number): Promise` 151 | > Returns all activities belonging to a file or folder. Use the `limit` argument to override the server-default. 152 | 153 | ### Users 154 | The following methods are available on `client.users`: 155 | 156 | `removeSubAdminFromGroup(userId: string, groupId: string): Promise` 157 | > Remove a user as a Sub Admin from a group. 158 | 159 | `addSubAdminToGroup(userId: string, groupId: string): Promise` 160 | > Add a user as a Sub Admin to a group. 161 | 162 | `resendWelcomeEmail(userId: string): Promise` 163 | > Resend the Welcome email to a user. 164 | 165 | `removeFromGroup(userId: string, groupId: string): Promise` 166 | > Remove a user from a group. 167 | 168 | `getSubAdminGroups(userId: string): Promise` 169 | > Gets a list of all the groups a user is a Sub Admin of. 170 | 171 | `setEnabled(userId: string, isEnabled: boolean): Promise` 172 | > Enables or disables a user. 173 | 174 | `addToGroup(userId: string, groupId: string): Promise` 175 | > Add a user to a group. 176 | 177 | `getGroups(userId: string): Promise` 178 | > Gets a list of all the groups a user is a member of. 179 | 180 | `delete(userId: string): Promise` 181 | > Delete a user. 182 | 183 | `edit(userId: string, field: OcsEditUserField, value: string): Promise` 184 | > Edit a single field of a user. 185 | 186 | `list(search?: string, limit?: number, offset?: number): Promise` 187 | > Gets a list of all users. Use the `limit` argument to override the server-default. 188 | 189 | `add(user: OcsNewUser): Promise` 190 | > Add a new user. 191 | 192 | `get(userId: string): Promise` 193 | > Gets the user information. 194 | 195 | ### Groups 196 | The following methods are available on `client.groups`: 197 | 198 | `getSubAdmins(groupId: string): Promise` 199 | > Gets a list of all the users that are a Sub Admin of the group. 200 | 201 | `getUsers(groupId: string): Promise` 202 | > Gets a list of all the users that are a member of the group. 203 | 204 | `delete(groupId: string): Promise` 205 | > Delete a group. 206 | 207 | `list(search?: string, limit?: number, offset?: number): Promise` 208 | > Gets a list of all groups. 209 | Use the `limit` argument to override the server-default. 210 | 211 | `add(groupId: string): Promise` 212 | > Add a new group. 213 | 214 | ### Shares 215 | The following methods are available on `client.shares`: 216 | 217 | `delete(shareId: string| number): Promise` 218 | > Delete a share. 219 | 220 | `list(path?: string, includeReshares?: boolean, showForSubFiles?: boolean): Promise` 221 | > Gets a list of all the shares. Use `path` to show all the shares for that specific file or folder. Use `includeReshares` to also include shares not belonging to the user. Use `showForSubFiles` to show the shares of the children instead. This will throw an error if the path is a file. 222 | 223 | `add: (path: string, shareType: OcsShareType, shareWith?: string, permissions?: OcsSharePermissions, password?: string, publicUpload?: boolean): Promise` 224 | > Add a new share. `shareWith` has to be filled if `shareType` is a `user` or `group`. Use `permissions` bit-wise to add several permissions. `OcsSharePermissions.default` will let the server decide the permissions. This will throw an error if the specific share already exists. Use `shares.edit` to edit an existing share. 225 | 226 | `get: (shareId: string|number): Promise` 227 | > Gets the share information. 228 | 229 | #### edit 230 | The following methods are available on `client.shares.edit`: 231 | 232 | `permissions(shareId: string|number, permissions: OcsSharePermissions): Promise` 233 | > Change the permissions. Use `permissions` bit-wise to add several permissions. 234 | 235 | `password(shareId: string|number, password: string): Promise` 236 | > Change the password. Only `OcsShareType.publicLink` uses passwords. 237 | 238 | `publicUpload(shareId: string|number, isPublicUpload: boolean): Promise` 239 | > Enable / disable public upload for public shares. 240 | 241 | `expireDate(shareId: string|number, expireDate: string): Promise` 242 | > Add an expire date to the share. If the expire date is in the past, Nextcloud will remove the share. 243 | 244 | `note(shareId: string|number, note: string): Promise` 245 | > Add a note to the share. 246 | 247 | ### Groupfolders 248 | 249 | To be able to use `groupfolders` interface, the [groupfolders](https://github.com/nextcloud/groupfolders) app needs to be downloaded and activated in the Nextcloud settings. 250 | The following methods are available on `client.groupfolders`: 251 | 252 | `getFolders: () => Promise` 253 | > Returns a list of all configured folders and their settings. 254 | 255 | `getFolder: (fid: number) => Promise` 256 | > Return a specific configured groupfolder and its settings, `null` if not found. 257 | 258 | `addFolder: (mountpoint: string) => Promise` 259 | > Create a new groupfolder with name `mountpoint` and returns its `id`. 260 | 261 | `removeFolder: (fid: number) => Promise` 262 | > Delete a groupfolder. Returns `true` if successful (even if the groupfolder didn't exist). 263 | 264 | `addGroup: (fid: number, gid: string) => Promise` 265 | > Give a group access to a groupfolder. 266 | 267 | `removeGroup: (fid: number, gid: string) => Promise` 268 | > Remove access from a group to a groupfolder. 269 | 270 | `setPermissions: (fid: number, gid: string, permissions: number) => Promise` 271 | > Set the permissions a group has in a groupfolder. The `permissions` parameter is a bitmask of [permissions constants](https://github.com/nextcloud/server/blob/b4f36d44c43aac0efdc6c70ff8e46473341a9bfe/lib/public/Constants.php#L65). 272 | 273 | `enableACL: (fid: number, enable: boolean) => Promise` 274 | > Enable/Disable groupfolder advanced permissions. 275 | 276 | `setManageACL: (fid: number, type: 'group' | 'user', id: string, manageACL: boolean) => Promise` 277 | > Grants/Removes a group or user the ability to manage a groupfolders' advanced permissions. 278 | > `mappingId`: the id of the group/user to be granted/removed access to/from the groupfolder 279 | > `mappingType`: 'group' or 'user' 280 | > `manageAcl`: true to grants ability to manage a groupfolders' advanced permissions, false to remove 281 | 282 | `setQuota: (fid: number, quota: number) => Promise` 283 | > Set the `quota` for a groupfolder in bytes (use `-3` for unlimited). 284 | 285 | `renameFolder: (fid: number, mountpoint: string) => Promise` 286 | > Change the name of a groupfolder to `mountpoint`. 287 | 288 | Note: If the `groupfolders` app is not activated, the requests are returning code `302`. The GET requests are redirected to the Location header (`/apps/dashboard/`) which makes it complicated to catch (returns `200` and `text/html` content type). The `client.groupfolders` methods would then throw with an error code `500` and a message "Unable to parse the response body as valid JSON". 289 | 290 | ## Exceptions 291 | 292 | ### NotFoundError 293 | Error indicating that the requested resource doesn't exist, or that the path leading to it doesn't exist in the case of writes. 294 | 295 | ### ForbiddenError 296 | Error indicating that Nextcloud denied the request. 297 | 298 | ### NextcloudError 299 | Generic wrapper for the HTTP errors returned by Nextcloud. 300 | 301 | ### OcsError 302 | Errors used by all OCS calls. 303 | It will return the reason why a request failed as well as a status code if it is available. 304 | 305 | ## Types 306 | ### ConnectionOptions 307 | ```javascript 308 | interface ConnectionOptions { 309 | url: string; 310 | username?: string; 311 | password?: string; 312 | } 313 | ``` 314 | 315 | ### WebDAV 316 | ```javascript 317 | interface FileDetails { 318 | creationDate?: Date; 319 | lastModified: Date; 320 | href: string; 321 | name: string; 322 | size: number; 323 | isDirectory: boolean; 324 | isFile: boolean; 325 | type: 'directory' | 'file'; 326 | } 327 | ``` 328 | 329 | ### OCS 330 | ```javascript 331 | interface OcsActivity { 332 | activityId: number; 333 | app: string; 334 | type: string; 335 | user: string; 336 | subject: string; 337 | subjectRich: []; 338 | message: string; 339 | messageRich: []; 340 | objectType: string; 341 | fileId: number; 342 | objectName: string; 343 | objects: {}; 344 | link: string; 345 | icon: string; 346 | datetime: Date; 347 | } 348 | 349 | interface OcsUser { 350 | id: string; 351 | enabled: boolean; 352 | lastLogin: number; 353 | email: string; 354 | displayname: string; 355 | phone: string; 356 | address: string; 357 | website: string; 358 | twitter: string; 359 | groups: string[]; 360 | language: string; 361 | locale: string; 362 | } 363 | 364 | interface OcsNewUser { 365 | userid: string; 366 | password?: string; 367 | email?: string; 368 | displayName?: string; 369 | groups?: string[]; 370 | subadmin?: string[]; 371 | quota?: number; 372 | language?: string; 373 | } 374 | 375 | type OcsEditUserField = 376 | 'password' | 377 | 'email' | 378 | 'displayname' | 379 | 'quota' | 380 | 'phone' | 381 | 'address' | 382 | 'website' | 383 | 'twitter' | 384 | 'locale' | 385 | 'language' ; 386 | 387 | enum OcsShareType { 388 | user = 0, 389 | group = 1, 390 | publicLink = 3, 391 | federatedCloudShare = 6, 392 | } 393 | 394 | enum OcsSharePermissions { 395 | default = -1, 396 | read = 1, 397 | update = 2, 398 | create = 4, 399 | delete = 8, 400 | share = 16, 401 | all = 31, 402 | } 403 | 404 | interface OcsShare { 405 | id: number; 406 | shareType: OcsShareType; 407 | shareTypeSystemName: string; 408 | ownerUserId: string; 409 | ownerDisplayName: string; 410 | permissions: OcsSharePermissions; 411 | permissionsText: string; 412 | sharedOn: Date; 413 | sharedOnTimestamp: number; 414 | parent: string; 415 | expiration: Date; 416 | token: string; 417 | fileOwnerUserId: string; 418 | fileOwnerDisplayName: string; 419 | note: string; 420 | label: string; 421 | path: string; 422 | itemType: 'file' | 'folder'; 423 | mimeType: string; 424 | storageId: string; 425 | storage: number; 426 | fileId: number; 427 | parentFileId: number; 428 | fileTarget: string; 429 | sharedWith: string; 430 | sharedWithDisplayName: string; 431 | mailSend: boolean; 432 | hideDownload: boolean; 433 | password?: string; 434 | sendPasswordByTalk?: boolean; 435 | url?: string; 436 | } 437 | 438 | type OcsEditShareField = 439 | 'permissions' | 440 | 'password' | 441 | 'expireDate' | 442 | 'note' ; 443 | 444 | interface OcsGroupfolderManageRule { 445 | type: 'group' | 'user' 446 | id: string; 447 | displayname: string; 448 | } 449 | 450 | interface OcsGroupfolder { 451 | id: number; 452 | mountPoint: string; 453 | groups: Record; 454 | quota: number; 455 | size: number; 456 | acl: boolean; 457 | manage?: OcsGroupfolderManageRule[]; 458 | } 459 | ``` 460 | 461 | ## Helpers 462 | 463 | `createFileDetailProperty(namespace: string, namespaceShort: string, element: string, nativeType?: boolean, defaultValue?: any): FileDetailProperty` 464 | > Creates a FileDetailProperty filled in with the supplied arguments, which can be used when using getFolderFileDetails. 465 | 466 | `createOwnCloudFileDetailProperty(element: string, nativeType?: boolean, defaultValue?: any): FileDetailProperty` 467 | > Uses createFileDetailProperty to request an OwnCloud property. 468 | 469 | `createNextCloudFileDetailProperty(element:string, nativeType?: boolean, defaultValue?: any): FileDetailProperty` 470 | > Uses createFileDetailProperty to request a Nextcloud property. 471 | 472 | ## Definitions 473 | 474 | ### fileId 475 | 476 | This is an OwnCloud property representing either a File or a Folder. It is own of the so-called `extraProperties` only returned by the `WebDAV` on request. See the following section for more details 477 | on `extraProperties`. 478 | 479 | ### WebDAV extraProperties 480 | 481 | `extraProperties` is an optional parameter that can be passed to both [`getFolderProperties`](#getfolderproperties) and [`getFolderFileDetails`](#getfolderfiledetails). The parameter consists of a list of optional properties that are not returned by the `WebDAV` interface by default. 482 | 483 | A simple example that requests the `fileId` of a directory on top of the standard properties returned by the `WebDAV` API would be: 484 | 485 | ```typescript 486 | const fileId = createOwnCloudFileDetailProperty('fileid', true); 487 | const documentList = await client.getFolderFileDetails('/Documents', [fileId]); 488 | for (const directory of documentList) { 489 | const folderId = directory.extraProperties.fileid; 490 | } 491 | ``` 492 | Which properties get returned by default and which are only available at request can be found in the [Nextcloud Documentation](https://docs.nextcloud.com/server/latest/developer_manual/client_apis/WebDAV/basic.html#requesting-properties). 493 | 494 | ### Sub Admin 495 | 496 | This is a Nextcloud term used to describe a user that has administrator rights for a group. 497 | 498 | ## Contributing 499 | 500 | Running tests is a little complicated right now, we're looking into improving this situation. While you can initiate tests using a normal `npm test`, you'll require `docker` and `docker-compose` to be installed in your path. 501 | -------------------------------------------------------------------------------- /source/ocs/ocs.ts: -------------------------------------------------------------------------------- 1 | import { rejectWithOcsError } from './helper'; 2 | import { ocsGetActivities } from './activity'; 3 | import { OcsConnection } from './ocs-connection'; 4 | import { promisify } from 'util'; 5 | 6 | import { 7 | OcsSharePermissions, 8 | OcsEditShareField, 9 | OcsEditUserField, 10 | OcsGroupfolder, 11 | OcsShareType, 12 | OcsActivity, 13 | OcsNewUser, 14 | OcsShare, 15 | OcsUser, 16 | } from './types'; 17 | 18 | import { 19 | ocsResendUserWelcomeEmail, 20 | ocsAddRemoveUserForGroup, 21 | ocsGetUserSubAdmins, 22 | ocsSetUserSubAdmin, 23 | ocsSetUserEnabled, 24 | ocsGetUserGroups, 25 | ocsDeleteUser, 26 | ocsListUsers, 27 | ocsEditUser, 28 | ocsAddUser, 29 | ocsGetUser, 30 | } from './user'; 31 | 32 | import { 33 | ocsGetGroupSubAdmins, 34 | ocsGetGroupUsers, 35 | ocsDeleteGroup, 36 | ocsListGroups, 37 | ocsAddGroup, 38 | } from './group'; 39 | 40 | import { 41 | ocsDeleteShare, 42 | ocsEditShare, 43 | ocsGetShares, 44 | ocsAddShare, 45 | ocsGetShare, 46 | } from './share'; 47 | 48 | import { 49 | ocsGetGroupfolders, 50 | ocsGetGroupfolder, 51 | ocsAddGroupfolder, 52 | ocsRemoveGroupfolder, 53 | ocsAddGroupfolderGroup, 54 | ocsRemoveGroupfolderGroup, 55 | ocsEnableOrDisableGroupfolderACL, 56 | ocsRenameGroupfolder, 57 | ocsSetGroupfolderQuota, 58 | ocsSetGroupfolderPermissions, 59 | ocsSetGroupfolderManageACL, 60 | } from './groupfolders'; 61 | 62 | import { 63 | NextcloudClientInterface, 64 | ConnectionOptions, 65 | } from '../types'; 66 | 67 | const promisifiedGetActivities = promisify(ocsGetActivities); 68 | 69 | const promisifiedResendUserWelcomeEmail = promisify(ocsResendUserWelcomeEmail); 70 | const promisifiedAddRemoveUserForGroup = promisify(ocsAddRemoveUserForGroup); 71 | const promisifiedGetUserSubAdmins = promisify(ocsGetUserSubAdmins); 72 | const promisifiedSetUserSubAdmin = promisify(ocsSetUserSubAdmin); 73 | const promisifiedSetUserEnabled = promisify(ocsSetUserEnabled); 74 | const promisifiedGetUserGroups = promisify(ocsGetUserGroups); 75 | const promisifiedDeleteUser = promisify(ocsDeleteUser); 76 | const promisifiedListUsers = promisify(ocsListUsers); 77 | const promisifiedEditUser = promisify(ocsEditUser); 78 | const promisifiedAddUser = promisify(ocsAddUser); 79 | const promisifiedGetUser = promisify(ocsGetUser); 80 | 81 | const promisifiedGetGroupSubAdmins = promisify(ocsGetGroupSubAdmins); 82 | const promisifiedGetGroupUsers = promisify(ocsGetGroupUsers); 83 | const promisifiedDeleteGroup = promisify(ocsDeleteGroup); 84 | const promisifiedListGroups = promisify(ocsListGroups); 85 | const promisifiedAddGroup = promisify(ocsAddGroup); 86 | 87 | const promisifiedDeleteShare = promisify(ocsDeleteShare); 88 | const promisifiedEditShare = promisify(ocsEditShare); 89 | const promisifiedGetShares = promisify(ocsGetShares); 90 | const promisifiedGetShare = promisify(ocsGetShare); 91 | const promisifiedAddShare = promisify(ocsAddShare); 92 | 93 | const promisifiedGetGroupfolders = promisify(ocsGetGroupfolders); 94 | const promisifiedGetGroupfolder = promisify(ocsGetGroupfolder); 95 | const promisifiedAddGroupfolder = promisify(ocsAddGroupfolder); 96 | const promisifiedRemoveGroupfolder = promisify(ocsRemoveGroupfolder); 97 | const promisifiedAddGroupfolderGroup = promisify(ocsAddGroupfolderGroup); 98 | const promisifiedRemoveGroupfolderGroup = promisify(ocsRemoveGroupfolderGroup); 99 | const promisifiedEnableOrDisableGroupfolderACL = promisify(ocsEnableOrDisableGroupfolderACL); 100 | const promisifiedRenameGroupfolder = promisify(ocsRenameGroupfolder); 101 | const promisifiedSetGroupfolderQuota = promisify(ocsSetGroupfolderQuota); 102 | const promisifiedSetGroupfolderPermissions = promisify(ocsSetGroupfolderPermissions); 103 | const promisifiedSetGroupfolderManageACL = promisify(ocsSetGroupfolderManageACL); 104 | 105 | export function configureOcsConnection(options: ConnectionOptions): void { 106 | const self: NextcloudClientInterface = this; 107 | 108 | self.ocsConnection = new OcsConnection({ 109 | url: options.url, 110 | username: options.username, 111 | password: options.password 112 | }); 113 | } 114 | 115 | export async function getActivities( 116 | connection: OcsConnection, 117 | fileId: number | string, 118 | sort?: 'asc' | 'desc', 119 | limit?: number, 120 | sinceActivityId?: number 121 | ) : Promise { 122 | let activities: Promise; 123 | 124 | try { 125 | activities = await promisifiedGetActivities.call( 126 | connection, 127 | (typeof fileId === 'string' ? parseInt(fileId, 10) : fileId), 128 | sort || 'desc', 129 | limit || -1, 130 | sinceActivityId || -1 131 | ); 132 | } catch (error) { 133 | activities = rejectWithOcsError(error, { 134 | message: 'Unable to get activities for', 135 | identifier: fileId, 136 | useMeta: false, 137 | customErrors: { 138 | [204]: 'The user has selected no activities to be listed in the stream', 139 | [304]: 'ETag/If-None-Match are the same or the end of the activity list was reached', 140 | [403]: 'The offset activity belongs to a different user or the user is not logged in', 141 | [404]: 'The filter is unknown' 142 | } 143 | }); 144 | } 145 | 146 | return activities; 147 | } 148 | 149 | export async function getUser( 150 | connection: OcsConnection, 151 | userId: string 152 | ) : Promise { 153 | let user: Promise; 154 | 155 | try { 156 | user = await promisifiedGetUser.call(connection, userId); 157 | } catch (error) { 158 | user = rejectWithOcsError(error, { 159 | message: 'Unable to find user', 160 | identifier: userId, 161 | useMeta: false 162 | }); 163 | } 164 | 165 | return user; 166 | } 167 | 168 | export async function setUserEnabled( 169 | connection: OcsConnection, 170 | userId: string, 171 | isEnabled: boolean 172 | ): Promise { 173 | let success: Promise; 174 | 175 | try { 176 | success = await promisifiedSetUserEnabled.call(connection, userId, isEnabled); 177 | } catch (error) { 178 | success = rejectWithOcsError(error, { 179 | message: `Unable to ${isEnabled ? 'enable' : 'disable'} user`, 180 | identifier: userId, 181 | useMeta: true, 182 | customErrors: { 183 | [101]: 'user does not exist' 184 | } 185 | }); 186 | } 187 | 188 | return success; 189 | } 190 | 191 | export async function editUser( 192 | connection: OcsConnection, 193 | userId: string, 194 | field: OcsEditUserField, 195 | value: string 196 | ): Promise { 197 | let userEdited: Promise; 198 | 199 | try { 200 | userEdited = await promisifiedEditUser.call(connection, userId, field, value); 201 | } catch (error) { 202 | userEdited = rejectWithOcsError(error, { 203 | message: 'Unable to edit user', 204 | identifier: userId, 205 | useMeta: true, 206 | expectedErrorCodes: [400, 401], 207 | customErrors: { 208 | [101]: 'user not found', 209 | [997]: 'possible reasons: Does it exist? Do you have the right permissions? Is the field valid?' 210 | } 211 | }); 212 | } 213 | 214 | return userEdited; 215 | } 216 | 217 | export async function getUserGroups( 218 | connection: OcsConnection, 219 | userId: string 220 | ): Promise { 221 | let groups: Promise; 222 | 223 | try { 224 | groups = await promisifiedGetUserGroups.call(connection, userId); 225 | } catch (error) { 226 | groups = rejectWithOcsError(error, { 227 | message: 'Unable to get groups for user', 228 | identifier: userId, 229 | useMeta: false 230 | }); 231 | } 232 | 233 | return groups; 234 | } 235 | 236 | export async function getUserSubAdmins( 237 | connection: OcsConnection, 238 | userId: string 239 | ): Promise { 240 | let subAdmins: Promise; 241 | 242 | try { 243 | subAdmins = await promisifiedGetUserSubAdmins.call(connection, userId); 244 | } catch (error) { 245 | subAdmins = rejectWithOcsError(error, { 246 | message: 'Unable to get sub-admins for user', 247 | identifier: userId, 248 | useMeta: true, 249 | expectedErrorCodes: [400], 250 | customErrors: { 251 | [101]: 'user does not exist' 252 | } 253 | }); 254 | } 255 | 256 | return subAdmins; 257 | } 258 | 259 | export async function resendUserWelcomeEmail( 260 | connection: OcsConnection, 261 | userId: string 262 | ): Promise { 263 | let success: Promise; 264 | 265 | try { 266 | success = await promisifiedResendUserWelcomeEmail.call(connection, userId); 267 | } catch (error) { 268 | success = rejectWithOcsError(error, { 269 | message: 'Unable to resend welcome email for user', 270 | identifier: userId, 271 | useMeta: true, 272 | expectedErrorCodes: [400], 273 | customErrors: { 274 | [101]: 'email address not available', 275 | [102]: 'sending email failed' 276 | } 277 | }); 278 | } 279 | 280 | return success; 281 | } 282 | 283 | export async function addRemoveUserForGroup( 284 | connection: OcsConnection, 285 | userId: string, 286 | groupId: string, 287 | toAdd: boolean 288 | ): Promise { 289 | let userModifiedForGroup: Promise; 290 | 291 | try { 292 | userModifiedForGroup = await promisifiedAddRemoveUserForGroup.call(connection, userId, groupId, toAdd); 293 | } catch (error) { 294 | userModifiedForGroup = rejectWithOcsError(error, { 295 | message: `Unable to ${toAdd ? 'add' : 'remove'} user '${userId}' ${toAdd ? 'to' : 'from'} group`, 296 | identifier: groupId, 297 | useMeta: true, 298 | expectedErrorCodes: [400], 299 | customErrors: { 300 | [101]: 'no group specified', 301 | [102]: 'group does not exist', 302 | [103]: 'user does not exist', 303 | [104]: 'insufficient privileges', 304 | } 305 | }); 306 | } 307 | 308 | return userModifiedForGroup; 309 | } 310 | 311 | export async function addRemoveUserSubAdminForGroup( 312 | connection: OcsConnection, 313 | userId: string, 314 | groupId: string, 315 | toAdd: boolean 316 | ): Promise { 317 | let subAdminModifiedForGroup: Promise; 318 | 319 | try { 320 | subAdminModifiedForGroup = await promisifiedSetUserSubAdmin.call(connection, userId, groupId, toAdd); 321 | } catch (error) { 322 | let customErrors = {}; 323 | if (toAdd) { 324 | customErrors[101] = 'user does not exist'; 325 | customErrors[102] = 'group does not exist'; 326 | } else { 327 | customErrors[101] = 'user or group does not exist'; 328 | customErrors[102] = 'user is not a sub-admin of the group'; 329 | } 330 | 331 | subAdminModifiedForGroup = rejectWithOcsError(error, { 332 | customErrors, 333 | message: `Unable to ${toAdd ? 'add' : 'remove'} user '${userId}' as sub-admin ${toAdd ? 'to' : 'from'} group`, 334 | identifier: groupId, 335 | useMeta: true, 336 | expectedErrorCodes: [400], 337 | }); 338 | } 339 | 340 | return subAdminModifiedForGroup; 341 | } 342 | 343 | export async function listUsers( 344 | connection: OcsConnection, 345 | search?: string, 346 | limit?: number, 347 | offset?: number 348 | ): Promise { 349 | let users: Promise; 350 | 351 | try { 352 | users = await promisifiedListUsers.call(connection, 353 | search || '', 354 | Number.isInteger(limit) ? limit : -1, 355 | Number.isInteger(offset) ? offset : -1 356 | ); 357 | } catch (error) { 358 | users = rejectWithOcsError(error, { 359 | message: 'Unable to list users', 360 | useMeta: false 361 | }); 362 | } 363 | 364 | return users; 365 | } 366 | 367 | export async function deleteUser( 368 | connection: OcsConnection, 369 | userId: string 370 | ): Promise { 371 | let userDeleted: Promise; 372 | 373 | try { 374 | userDeleted = await promisifiedDeleteUser.call(connection, userId); 375 | } catch (error) { 376 | userDeleted = rejectWithOcsError(error, { 377 | message: 'Unable to delete user', 378 | identifier: userId, 379 | useMeta: true, 380 | expectedErrorCodes: [400], 381 | customErrors: { 382 | [101]: 'user does not exist' 383 | } 384 | }); 385 | } 386 | 387 | return userDeleted; 388 | } 389 | 390 | export async function addUser( 391 | connection: OcsConnection, 392 | user: OcsNewUser 393 | ): Promise { 394 | let userAdded: Promise; 395 | 396 | try { 397 | userAdded = await promisifiedAddUser.call(connection, user); 398 | } catch (error) { 399 | userAdded = rejectWithOcsError(error, { 400 | message: 'Unable to add user', 401 | identifier: (user && user.userid ? user.userid : ''), 402 | useMeta: true, 403 | expectedErrorCodes: [400], 404 | customErrors: { 405 | [102]: 'username already exists', 406 | [103]: 'unknown error occurred whilst adding the user', 407 | [104]: 'group does not exist', 408 | [105]: 'insufficient privileges for group', 409 | [106]: 'no group specified (required for sub-admins', 410 | [108]: 'password and email empty. Must set password or an email', 411 | [109]: 'invitation email cannot be send' 412 | } 413 | }); 414 | } 415 | 416 | return userAdded; 417 | } 418 | 419 | export async function listGroups( 420 | connection: OcsConnection, 421 | search?: string, 422 | limit?: number, 423 | offset?: number 424 | ): Promise { 425 | let groups: Promise; 426 | 427 | try { 428 | groups = await promisifiedListGroups.call( 429 | connection, 430 | search || '', 431 | Number.isInteger(limit) ? limit : -1, 432 | Number.isInteger(offset) ? offset : -1 433 | ); 434 | } catch (error) { 435 | groups = rejectWithOcsError(error, { 436 | message: 'Unable to list groups', 437 | useMeta: false 438 | }); 439 | } 440 | 441 | return groups; 442 | } 443 | 444 | export async function addGroup( 445 | connection: OcsConnection, 446 | groupId: string 447 | ): Promise { 448 | let groupAdded: Promise; 449 | 450 | try { 451 | groupAdded = await promisifiedAddGroup.call(connection, groupId); 452 | } catch (error) { 453 | groupAdded = rejectWithOcsError(error, { 454 | message: 'Unable to add group', 455 | identifier: groupId, 456 | useMeta: true, 457 | expectedErrorCodes: [400], 458 | customErrors: { 459 | [102]: 'group already exists', 460 | [103]: 'failed to add the group' 461 | } 462 | }); 463 | } 464 | 465 | return groupAdded; 466 | } 467 | 468 | export async function deleteGroup( 469 | connection: OcsConnection, 470 | groupId: string 471 | ): Promise { 472 | let groupDeleted: Promise; 473 | 474 | try { 475 | groupDeleted = await promisifiedDeleteGroup.call(connection, groupId); 476 | } catch (error) { 477 | groupDeleted = rejectWithOcsError(error, { 478 | message: 'Unable to delete group', 479 | identifier: groupId, 480 | useMeta: true, 481 | expectedErrorCodes: [400], 482 | customErrors: { 483 | [101]: 'group does not exist', 484 | [102]: 'failed to delete group' 485 | } 486 | }); 487 | } 488 | 489 | return groupDeleted; 490 | } 491 | 492 | export async function getGroupUsers( 493 | connection: OcsConnection, 494 | groupId: string 495 | ): Promise { 496 | let users: Promise; 497 | 498 | try { 499 | users = await promisifiedGetGroupUsers.call(connection, groupId); 500 | } catch (error) { 501 | users = rejectWithOcsError(error, { 502 | message: 'Unable to list users for group', 503 | identifier: groupId, 504 | useMeta: false, 505 | expectedErrorCodes: [404], 506 | customErrors: { 507 | [404]: 'the group could not be found' 508 | } 509 | }); 510 | } 511 | 512 | return users; 513 | } 514 | 515 | export async function getGroupSubAdmins( 516 | connection: OcsConnection, 517 | groupId: string 518 | ): Promise { 519 | let subAdmins: Promise; 520 | 521 | try { 522 | subAdmins = await promisifiedGetGroupSubAdmins.call(connection, groupId); 523 | } catch (error) { 524 | subAdmins = rejectWithOcsError(error, { 525 | message: 'Unable to list sub-admins for group', 526 | identifier: groupId, 527 | useMeta: true, 528 | expectedErrorCodes: [400], 529 | customErrors: { 530 | [101]: 'group does not exist' 531 | } 532 | }); 533 | } 534 | 535 | return subAdmins; 536 | } 537 | 538 | export async function getShares( 539 | connection: OcsConnection, 540 | path?: string, 541 | includeReshares?: boolean, 542 | showForSubFiles?: boolean 543 | ): Promise { 544 | let shares: Promise; 545 | 546 | try { 547 | shares = await promisifiedGetShares.call(connection, 548 | path || '', 549 | (includeReshares !== undefined ? includeReshares : false), 550 | (showForSubFiles !== undefined ? showForSubFiles : false) 551 | ); 552 | } catch (error) { 553 | shares = rejectWithOcsError(error, { 554 | message: 'Unable to get shares for', 555 | identifier: path, 556 | useMeta: true, 557 | expectedErrorCodes: [400, 404], 558 | customErrors: { 559 | [400]: 'unable to show sub-files as this is not a directory', 560 | [404]: 'file/folder doesn\'t exist' 561 | } 562 | }); 563 | } 564 | 565 | return shares; 566 | } 567 | 568 | export async function getShare( 569 | connection: OcsConnection, 570 | shareId: number | string 571 | ): Promise { 572 | let share: Promise; 573 | 574 | try { 575 | share = await promisifiedGetShare.call(connection, shareId); 576 | } catch (error) { 577 | share = rejectWithOcsError(error, { 578 | message: 'Unable to get share', 579 | identifier: shareId, 580 | useMeta: true, 581 | expectedErrorCodes: [404] 582 | }); 583 | } 584 | 585 | return share; 586 | } 587 | 588 | export async function deleteShare( 589 | connection: OcsConnection, 590 | shareId: number | string 591 | ): Promise { 592 | let shareDeleted: Promise; 593 | 594 | try { 595 | shareDeleted = await promisifiedDeleteShare.call(connection, shareId); 596 | } catch (error) { 597 | shareDeleted = rejectWithOcsError(error, { 598 | message: 'Unable to delete share', 599 | identifier: shareId, 600 | useMeta: true, 601 | expectedErrorCodes: [404], 602 | customErrors: { 603 | [404]: 'invalid shareId or the share doesn\'t exist' 604 | } 605 | }); 606 | } 607 | 608 | return shareDeleted; 609 | } 610 | 611 | export async function addShare( 612 | connection: OcsConnection, 613 | path: string, 614 | shareType: OcsShareType, 615 | shareWith?: string, 616 | permissions?: OcsSharePermissions, 617 | password?: string, 618 | publicUpload?: boolean, 619 | ): Promise { 620 | let addedShare: Promise; 621 | 622 | try { 623 | addedShare = await promisifiedAddShare.call(connection, 624 | path, 625 | shareType, 626 | shareWith || '', 627 | (permissions !== undefined ? permissions : OcsSharePermissions.default), 628 | password || '', 629 | (publicUpload !== undefined ? publicUpload : false), 630 | ); 631 | } catch (error) { 632 | addedShare = rejectWithOcsError(error, { 633 | message: 'Unable to add share', 634 | identifier: path, 635 | useMeta: true, 636 | expectedErrorCodes: [403, 404] 637 | }); 638 | } 639 | 640 | return addedShare; 641 | } 642 | 643 | export function editShare( 644 | connection: OcsConnection, 645 | shareId: number | string 646 | ) { 647 | return { 648 | async permissions(permissions: OcsSharePermissions): Promise { 649 | return await setFieldValue(connection, shareId, 'permissions', permissions); 650 | }, 651 | 652 | async password(password: string): Promise { 653 | return await setFieldValue(connection, shareId, 'password', password); 654 | }, 655 | 656 | async publicUpload(isPublicUpload: boolean): Promise { 657 | return await setFieldValue(connection, shareId, 'publicUpload', isPublicUpload); 658 | }, 659 | 660 | async expireDate(expireDate: string): Promise { 661 | return await setFieldValue(connection, shareId, 'expireDate', expireDate); 662 | }, 663 | 664 | async note(note: string): Promise { 665 | return await setFieldValue(connection, shareId, 'note', note); 666 | } 667 | }; 668 | 669 | async function setFieldValue( 670 | connection: OcsConnection, 671 | shareId: number | string, 672 | field: OcsEditShareField, 673 | value: any 674 | ): Promise { 675 | let editedShare: Promise; 676 | 677 | try { 678 | editedShare = await promisifiedEditShare.call(connection, shareId, field, String(value)); 679 | } catch (error) { 680 | editedShare = rejectWithOcsError(error, { 681 | message: `Unable to edit '${field}' of share`, 682 | identifier: shareId, 683 | useMeta: true, 684 | expectedErrorCodes: [400, 404] 685 | }); 686 | } 687 | 688 | return editedShare; 689 | } 690 | } 691 | 692 | export async function getGroupfolders( 693 | connection: OcsConnection, 694 | ): Promise { 695 | let groupfolders: Promise; 696 | 697 | try { 698 | groupfolders = await promisifiedGetGroupfolders.call(connection); 699 | } catch (error) { 700 | groupfolders = rejectWithOcsError(error, { 701 | message: 'Unable to list groupfolders', 702 | useMeta: true, 703 | expectedErrorCodes: [500], 704 | }); 705 | } 706 | 707 | return groupfolders; 708 | } 709 | 710 | export async function getGroupfolder( 711 | connection: OcsConnection, 712 | groupfolderId: number, 713 | ): Promise { 714 | let groupfolder: Promise; 715 | 716 | try { 717 | groupfolder = await promisifiedGetGroupfolder.call(connection, groupfolderId); 718 | } catch (error) { 719 | groupfolder = rejectWithOcsError(error, { 720 | message: 'Unable to get groupfolder', 721 | identifier: groupfolderId, 722 | useMeta: true, 723 | expectedErrorCodes: [500], 724 | }); 725 | } 726 | 727 | return groupfolder; 728 | } 729 | 730 | export async function addGroupfolder( 731 | connection: OcsConnection, 732 | mountpoint: string, 733 | ): Promise { 734 | let addedGroupfolderId: Promise; 735 | 736 | try { 737 | addedGroupfolderId = await promisifiedAddGroupfolder.call(connection, mountpoint); 738 | } catch (error) { 739 | addedGroupfolderId = rejectWithOcsError(error, { 740 | message: 'Unable to create groupfolder', 741 | identifier: mountpoint, 742 | useMeta: true, 743 | expectedErrorCodes: [500], 744 | }); 745 | } 746 | 747 | return addedGroupfolderId; 748 | } 749 | 750 | export async function removeGroupfolder( 751 | connection: OcsConnection, 752 | groupfolderId: number, 753 | ): Promise { 754 | let groupfolderDeleted: Promise; 755 | 756 | try { 757 | groupfolderDeleted = await promisifiedRemoveGroupfolder.call(connection, groupfolderId); 758 | } catch (error) { 759 | groupfolderDeleted = rejectWithOcsError(error, { 760 | message: 'Unable to delete groupfolder', 761 | identifier: groupfolderId, 762 | useMeta: true, 763 | expectedErrorCodes: [500], 764 | }); 765 | } 766 | 767 | return groupfolderDeleted; 768 | } 769 | 770 | export async function addGroupfolderGroup( 771 | connection: OcsConnection, 772 | groupfolderId: number, 773 | groupId: string, 774 | ): Promise { 775 | let groupfolderGroupAdded: Promise; 776 | 777 | try { 778 | groupfolderGroupAdded = await promisifiedAddGroupfolderGroup.call(connection, groupfolderId, groupId); 779 | } catch (error) { 780 | groupfolderGroupAdded = rejectWithOcsError(error, { 781 | message: 'Unable to add group to groupfolder', 782 | identifier: groupfolderId, 783 | useMeta: true, 784 | expectedErrorCodes: [500], 785 | }); 786 | } 787 | 788 | return groupfolderGroupAdded; 789 | } 790 | 791 | export async function removeGroupfolderGroup( 792 | connection: OcsConnection, 793 | groupfolderId: number, 794 | groupId: string, 795 | ): Promise { 796 | let groupfolderGroupRemoved: Promise; 797 | 798 | try { 799 | groupfolderGroupRemoved = await promisifiedRemoveGroupfolderGroup.call(connection, groupfolderId, groupId); 800 | } catch (error) { 801 | groupfolderGroupRemoved = rejectWithOcsError(error, { 802 | message: 'Unable to remove group from groupfolder', 803 | identifier: groupfolderId, 804 | useMeta: true, 805 | expectedErrorCodes: [500], 806 | }); 807 | } 808 | 809 | return groupfolderGroupRemoved; 810 | } 811 | 812 | export async function setGroupfolderPermissions( 813 | connection: OcsConnection, 814 | groupfolderId: number, 815 | groupId: string, 816 | permissions: number, 817 | ): Promise { 818 | let groupfolderPermissionsSet: Promise; 819 | 820 | try { 821 | groupfolderPermissionsSet = await promisifiedSetGroupfolderPermissions.call(connection, groupfolderId, groupId, permissions); 822 | } catch (error) { 823 | groupfolderPermissionsSet = rejectWithOcsError(error, { 824 | message: 'Unable to set groupfolder permissions', 825 | identifier: groupfolderId, 826 | useMeta: true, 827 | expectedErrorCodes: [500], 828 | }); 829 | } 830 | 831 | return groupfolderPermissionsSet; 832 | } 833 | 834 | export async function enableGroupfolderACL( 835 | connection: OcsConnection, 836 | groupfolderId: number, 837 | enable: boolean, 838 | ): Promise { 839 | let groupfolderACLEnabled: Promise; 840 | 841 | try { 842 | groupfolderACLEnabled = await promisifiedEnableOrDisableGroupfolderACL.call(connection, groupfolderId, enable); 843 | } catch (error) { 844 | groupfolderACLEnabled = rejectWithOcsError(error, { 845 | message: 'Unable to enable ACL for groupfolder', 846 | identifier: groupfolderId, 847 | useMeta: true, 848 | expectedErrorCodes: [500], 849 | }); 850 | } 851 | 852 | return groupfolderACLEnabled; 853 | } 854 | 855 | export async function setGroupfolderManageACL( 856 | connection: OcsConnection, 857 | groupfolderId: number, 858 | type: 'group' | 'user', 859 | id: string, 860 | manageACL: boolean, 861 | ): Promise { 862 | let groupfolderManageACLSet: Promise; 863 | 864 | try { 865 | groupfolderManageACLSet = await promisifiedSetGroupfolderManageACL.call(connection, groupfolderId, type, id, manageACL); 866 | } catch (error) { 867 | groupfolderManageACLSet = rejectWithOcsError(error, { 868 | message: 'Unable to set groupfolder manage ACL settings', 869 | identifier: groupfolderId, 870 | useMeta: true, 871 | expectedErrorCodes: [500], 872 | }); 873 | } 874 | 875 | return groupfolderManageACLSet; 876 | } 877 | 878 | export async function setGroupfolderQuota( 879 | connection: OcsConnection, 880 | groupfolderId: number, 881 | quota: number, 882 | ): Promise { 883 | let groupfolderQuotaSet: Promise; 884 | 885 | try { 886 | groupfolderQuotaSet = await promisifiedSetGroupfolderQuota.call(connection, groupfolderId, quota); 887 | } catch (error) { 888 | groupfolderQuotaSet = rejectWithOcsError(error, { 889 | message: 'Unable to set groupfolder quota', 890 | identifier: groupfolderId, 891 | useMeta: true, 892 | expectedErrorCodes: [500], 893 | }); 894 | } 895 | 896 | return groupfolderQuotaSet; 897 | } 898 | 899 | export async function renameGroupfolder( 900 | connection: OcsConnection, 901 | groupfolderId: number, 902 | mountpoint: string, 903 | ): Promise { 904 | let groupfolderRenamed: Promise; 905 | 906 | try { 907 | groupfolderRenamed = await promisifiedRenameGroupfolder.call(connection, groupfolderId, mountpoint); 908 | } catch (error) { 909 | groupfolderRenamed = rejectWithOcsError(error, { 910 | message: 'Unable to rename groupfolder', 911 | identifier: groupfolderId, 912 | useMeta: true, 913 | expectedErrorCodes: [500], 914 | }); 915 | } 916 | 917 | return groupfolderRenamed; 918 | } 919 | -------------------------------------------------------------------------------- /tests/webdav-jest.ts: -------------------------------------------------------------------------------- 1 | import { NotFoundError, OcsError } from '../source/errors'; 2 | import NextcloudClient from '../source/client'; 3 | import configuration from './configuration'; 4 | import Stream from 'stream'; 5 | import { Request } from 'request'; 6 | import { join } from 'path'; 7 | 8 | import { 9 | createFileDetailProperty, 10 | createOwnCloudFileDetailProperty, 11 | createNextCloudFileDetailProperty 12 | } from '../source/helper'; 13 | import { OcsNewUser, OcsShareType, OcsSharePermissions } from '../source/ocs/types'; 14 | 15 | describe('Webdav integration', function testWebdavIntegration() { 16 | const client = new NextcloudClient(configuration.connectionOptions); 17 | 18 | beforeEach(async () => { 19 | const files = await client.getFiles('/'); 20 | 21 | await Promise.all(files.map(async function (file) { 22 | await client.remove(`/${file}`); 23 | })); 24 | }); 25 | 26 | describe('checkConnectivity()', () => { 27 | it('should return false if there is no connectivity', async () => { 28 | const badClient = new NextcloudClient(Object.assign({}, configuration, { 29 | url: 'http://127.0.0.1:65530' 30 | })); 31 | 32 | expect(await client.checkConnectivity()).toBeTruthy(); 33 | expect(await badClient.checkConnectivity()).toBe(false); 34 | }); 35 | }); 36 | 37 | describe('exists(path)', () => { 38 | it('should return true if the given resource exists, false otherwise', async () => { 39 | const path = randomRootPath(); 40 | 41 | expect(await client.exists(path)).toBe(false); 42 | 43 | await client.put(path, ''); 44 | 45 | expect(await client.exists(path)).toBeTruthy(); 46 | 47 | await client.remove(path); 48 | }); 49 | 50 | it('should not crash for nested folders', async () => { 51 | const path = `${randomRootPath()}${randomRootPath()}`; 52 | 53 | expect(await client.exists(path)).toBe(false); 54 | }); 55 | }); 56 | 57 | describe('404s', () => { 58 | it('should throw 404s when a resource is not found', async () => { 59 | const path = randomRootPath(); 60 | const path2 = randomRootPath(); 61 | 62 | const nestedPath = `${path}${path2}`; 63 | 64 | const readStream = new Stream.Readable(); 65 | const writeStream = new Stream.Writable(); 66 | 67 | try { await client.get(path); } catch (error) { expect(error instanceof NotFoundError).toBeTruthy(); } 68 | try { await client.getFiles(path); } catch (error) { expect(error instanceof NotFoundError).toBeTruthy(); } 69 | try { await client.put(nestedPath, ''); } catch (error) { expect(error instanceof NotFoundError).toBeTruthy(); } 70 | try { await client.rename(path, path2); } catch (error) { expect(error instanceof NotFoundError).toBeTruthy(); } 71 | try { await client.getReadStream(path); } catch (error) { expect(error instanceof NotFoundError).toBeTruthy(); } 72 | try { await client.getWriteStream(nestedPath); } catch (error) { expect(error instanceof NotFoundError).toBeTruthy(); } 73 | try { await client.uploadFromStream(nestedPath, readStream); } catch (error) { expect(error instanceof NotFoundError).toBeTruthy(); } 74 | try { await client.downloadToStream(nestedPath, writeStream); } catch (error) { expect(error instanceof NotFoundError).toBeTruthy(); } 75 | }); 76 | }); 77 | 78 | describe('put & get', () => { 79 | it('should allow to save and get files without streaming', async () => { 80 | const path = randomRootPath(); 81 | const string = 'test'; 82 | 83 | expect(await client.exists(path)).toBe(false); 84 | 85 | await client.put(path, string); 86 | 87 | expect((await client.get(path)).toString()).toBe(string); 88 | 89 | await client.remove(path); 90 | }); 91 | 92 | it('should save a Buffer and get the file without streaming', async () => { 93 | const path = randomRootPath(); 94 | const string = 'tėŠt àáâèéî'; 95 | const buffer = Buffer.from(string); 96 | 97 | expect(await client.exists(path)).toBe(false); 98 | 99 | await client.put(path, buffer); 100 | 101 | expect((await client.get(path)).toString()).toBe(string); 102 | 103 | await client.remove(path); 104 | }); 105 | }); 106 | 107 | describe('remove(path)', () => { 108 | it('should remove simple files properly', async () => { 109 | const path = randomRootPath(); 110 | 111 | expect(await client.exists(path)).toBe(false); 112 | 113 | await client.put(path, ''); 114 | 115 | expect(await client.exists(path)).toBeTruthy(); 116 | 117 | await client.remove(path); 118 | 119 | expect(await client.exists(path)).toBe(false); 120 | }); 121 | 122 | it('should remove folders recursively', async () => { 123 | const path = randomRootPath(); 124 | 125 | const file = `${path}${path}`; 126 | 127 | await client.touchFolder(path); 128 | 129 | expect(await client.exists(path)).toBeTruthy(); 130 | 131 | await client.put(file, ''); 132 | 133 | await client.remove(path); 134 | 135 | expect(await client.exists(file)).toBe(false); 136 | expect(await client.exists(path)).toBe(false); 137 | }); 138 | }); 139 | 140 | describe('touchFolder(path)', () => { 141 | it('should create folders', async () => { 142 | const path = randomRootPath(); 143 | 144 | expect(await client.exists(path)).toBe(false); 145 | 146 | await client.touchFolder(path); 147 | 148 | expect(await client.exists(path)).toBeTruthy(); 149 | 150 | await client.remove(path); 151 | }); 152 | 153 | it('should allow folders with spaces in their names', async () => { 154 | const path = `${randomRootPath()} test`; 155 | 156 | await client.touchFolder(path); 157 | 158 | expect(await client.exists(path)).toBeTruthy(); 159 | 160 | await client.remove(path); 161 | }); 162 | 163 | it('should not complain if the folder already exists', async () => { 164 | const path = `${randomRootPath()} test`; 165 | 166 | await client.touchFolder(path); 167 | await client.touchFolder(path); 168 | 169 | expect(await client.exists(path)).toBeTruthy(); 170 | 171 | await client.remove(path); 172 | }); 173 | 174 | it('should allow folders with accented characters', async () => { 175 | const path = `${randomRootPath()} testé`; 176 | 177 | await client.touchFolder(path); 178 | 179 | expect(await client.exists(path)).toBeTruthy(); 180 | 181 | await client.remove(path); 182 | }); 183 | }); 184 | 185 | describe('getFiles(path)', () => { 186 | it('should retrieve lists of files in a given folder', async () => { 187 | const path = randomRootPath(); 188 | 189 | const fileName1 = 'file1'; 190 | const fileName2 = 'file2'; 191 | 192 | const file1 = `${path}/${fileName1}`; 193 | const file2 = `${path}/${fileName2}`; 194 | 195 | await client.touchFolder(path); 196 | await client.put(file1, ''); 197 | await client.put(file2, ''); 198 | 199 | expect(await client.exists(path)).toBeTruthy(); 200 | expect(await client.exists(file1)).toBeTruthy(); 201 | expect(await client.exists(file2)).toBeTruthy(); 202 | 203 | const files = await client.getFiles(path); 204 | 205 | expect(files.length).toBe(2); 206 | expect(files.includes(fileName1)).toBeTruthy(); 207 | expect(files.includes(fileName2)).toBeTruthy(); 208 | 209 | await client.remove(path); 210 | }); 211 | }); 212 | 213 | describe('getFolderFileDetails(path)', () => { 214 | it('should retrieve lists of files in a given folder', async () => { 215 | const path = randomRootPath(); 216 | 217 | const fileName1 = 'file1'; 218 | const fileName2 = 'file2'; 219 | 220 | const file1 = `${path}/${fileName1}`; 221 | const file2 = `${path}/${fileName2}`; 222 | 223 | await client.touchFolder(path); 224 | await client.touchFolder(file1); 225 | await client.put(file2, ''); 226 | 227 | const files = await client.getFolderFileDetails(path); 228 | 229 | expect(files.length).toBe(2); 230 | 231 | expect(files[0].isFile).toBeFalsy(); 232 | expect(files[0].name).toBe(fileName1); 233 | expect(files[0].isDirectory).toBeTruthy(); 234 | expect(files[0].creationDate).toBeFalsy(); 235 | expect(files[0].lastModified).toBeTruthy(); 236 | expect(files[0].href).toBe(`/remote.php/dav/files/nextcloud${path}/${fileName1}`); 237 | 238 | expect(files[1].isFile).toBeTruthy(); 239 | expect(files[1].name).toBe(fileName2); 240 | expect(files[1].isDirectory).toBeFalsy(); 241 | expect(files[1].creationDate).toBeFalsy(); 242 | expect(files[1].lastModified).toBeTruthy(); 243 | expect(files[1].href).toBe(`/remote.php/dav/files/nextcloud${path}/${fileName2}`); 244 | 245 | await client.remove(path); 246 | }); 247 | 248 | it('should retrieve the properties of a folder', async () => { 249 | const path = randomRootPath(); 250 | 251 | await client.touchFolder(path); 252 | const properties = await client.getFolderProperties(path, [ 253 | createOwnCloudFileDetailProperty('fileid', true), 254 | createOwnCloudFileDetailProperty('size', true), 255 | createOwnCloudFileDetailProperty('owner-id') 256 | ]); 257 | 258 | expect(properties['oc:owner-id'].content).toBe('nextcloud'); 259 | 260 | await client.remove(path); 261 | }); 262 | }); 263 | 264 | describe('createFolderHierarchy(path)', () => { 265 | it('should create hierarchies properly, even when part of it already exists', async () => { 266 | const path = randomRootPath(); 267 | 268 | const subFolder1 = 'sub1'; 269 | const subFolder2 = 'sub2'; 270 | const subFolder3 = 'sub3'; 271 | 272 | await client.touchFolder(path); 273 | 274 | const subFolder1Path = `${path}/${subFolder1}`; 275 | 276 | const subFolder2Path = `${subFolder1Path}/${subFolder2}`; 277 | 278 | const subFolder3Path = `${subFolder2Path}/${subFolder3}`; 279 | 280 | await client.createFolderHierarchy(subFolder3Path); 281 | 282 | expect(await client.exists(path)).toBeTruthy(); 283 | expect(await client.exists(subFolder1Path)).toBeTruthy(); 284 | expect(await client.exists(subFolder2Path)).toBeTruthy(); 285 | expect(await client.exists(subFolder3Path)).toBeTruthy(); 286 | 287 | await client.remove(path); 288 | }, 10000); 289 | }); 290 | 291 | describe('rename(path, newName)', () => { 292 | it('should work on simple files', async () => { 293 | const source = randomRootPath(); 294 | const renamed = randomRootPath().slice(1); 295 | 296 | const renamedPath = `/${renamed}`; 297 | 298 | await client.put(source, ''); 299 | 300 | expect(await client.exists(source)).toBeTruthy(); 301 | 302 | await client.rename(source, renamed); 303 | 304 | expect(await client.exists(source)).toBe(false); 305 | expect(await client.exists(renamedPath)).toBeTruthy(); 306 | 307 | await client.remove(renamedPath); 308 | }); 309 | 310 | it('should work on folders too', async () => { 311 | const source = randomRootPath(); 312 | const renamed = randomRootPath().slice(1); 313 | 314 | const renamedPath = `/${renamed}`; 315 | 316 | await client.touchFolder(source); 317 | 318 | expect(await client.exists(source)).toBeTruthy(); 319 | 320 | await client.rename(source, renamed); 321 | 322 | expect(await client.exists(source)).toBe(false); 323 | expect(await client.exists(renamedPath)).toBeTruthy(); 324 | 325 | await client.remove(renamedPath); 326 | }); 327 | }); 328 | 329 | describe('move(path, newName)', () => { 330 | it('should work on simple files', async () => { 331 | const folder = randomRootPath(); 332 | const source = randomRootPath(); 333 | const renamed = randomRootPath().slice(1); 334 | 335 | const renamedPath = `/${folder}/${renamed}`; 336 | 337 | await client.createFolderHierarchy(folder); 338 | 339 | await client.put(source, ''); 340 | 341 | expect(await client.exists(source)).toBeTruthy(); 342 | 343 | await client.move(source, renamedPath); 344 | 345 | expect(await client.exists(source)).toBe(false); 346 | expect(await client.exists(renamedPath)).toBeTruthy(); 347 | 348 | await client.remove(renamedPath); 349 | }); 350 | 351 | it('should work on folders too', async () => { 352 | const folder = randomRootPath(); 353 | const source = randomRootPath(); 354 | const file = randomRootPath(); 355 | const renamed = randomRootPath().slice(1); 356 | 357 | const sourceFilePath = `${source}${file}`; 358 | const renamedFolderPath = `${folder}/${renamed}`; 359 | 360 | const renamedPathFile = `${renamedFolderPath}${file}`; 361 | 362 | await client.createFolderHierarchy(folder); 363 | await client.createFolderHierarchy(source); 364 | 365 | await client.put(sourceFilePath, ''); 366 | 367 | expect(await client.exists(source)).toBeTruthy(); 368 | 369 | await client.move(source, renamedFolderPath); 370 | 371 | expect(await client.exists(source)).toBe(false); 372 | expect(await client.exists(renamedPathFile)).toBeTruthy(); 373 | expect(await client.exists(renamedFolderPath)).toBeTruthy(); 374 | 375 | await client.remove(renamedFolderPath); 376 | }); 377 | }); 378 | 379 | describe('getReadStream(path)', () => { 380 | it('should be able to stream files off of Nextcloud instances', async () => { 381 | const string = 'test'; 382 | const path = randomRootPath(); 383 | 384 | let data = ''; 385 | 386 | await client.put(path, string); 387 | 388 | const stream = await client.getReadStream(path); 389 | 390 | stream.on('data', chunk => data += chunk.toString()); 391 | 392 | await new Promise((resolve, reject) => { 393 | stream.on('end', resolve); 394 | stream.on('error', reject); 395 | }); 396 | 397 | expect(data).toBe(string); 398 | 399 | await client.remove(path); 400 | }); 401 | }); 402 | 403 | describe('getWriteStream(path)', () => { 404 | it('should pipe readable streams to the Nextcloud instance', async () => { 405 | const string = 'test'; 406 | const path = randomRootPath(); 407 | 408 | const stream : Request = await client.getWriteStream(path); 409 | 410 | expect(stream instanceof Stream).toBeTruthy(); 411 | 412 | await new Promise((resolve, reject) => { 413 | stream.on('end', resolve); 414 | stream.on('error', reject); 415 | 416 | stream.write(string); 417 | stream.end(); 418 | }); 419 | 420 | expect(await client.get(path)).toBe(string); 421 | 422 | await client.remove(path); 423 | }); 424 | }); 425 | 426 | describe('uploadFromStream(targetPath, readStream)', () => { 427 | it('should pipe from readable streams to the Nextcloud instance', async () => { 428 | const string = 'test'; 429 | const path = randomRootPath(); 430 | 431 | const readStream = getReadStream(string); 432 | 433 | await client.uploadFromStream(path, readStream); 434 | 435 | expect(await client.get(path)).toBe(string); 436 | 437 | await client.remove(path); 438 | }); 439 | }); 440 | 441 | describe('downloadToSream(sourcePath, writeStream)', () => { 442 | it('should pipe into provided writable streams from the Nextcloud instance', async () => { 443 | const path = randomRootPath(); 444 | const string = 'test'; 445 | const readStream = getReadStream(string); 446 | await client.uploadFromStream(path, readStream); 447 | 448 | const writeStream = getWriteStream(); 449 | 450 | writeStream.on('testchunk', (...args) => { 451 | expect(args[0].toJSON()).toEqual({ data: [116, 101, 115, 116], type: 'Buffer' }); 452 | }); 453 | 454 | await client.downloadToStream(path, writeStream); 455 | 456 | await client.remove(path); 457 | }); 458 | }); 459 | 460 | describe('Path reservation', () => { 461 | it('should allow saving a file with empty contents, then getting a write stream for it immediately', async () => { 462 | const path = randomRootPath(); 463 | 464 | await client.put(path, ''); 465 | 466 | const writeStream: Request = await client.getWriteStream(path); 467 | 468 | const writtenStream = getReadStream('test'); 469 | 470 | const completionPromise = new Promise((resolve, reject) => { 471 | writeStream.on('end', resolve); 472 | writeStream.on('error', reject); 473 | }); 474 | 475 | writtenStream.pipe(writeStream); 476 | 477 | await completionPromise; 478 | }); 479 | }); 480 | 481 | describe('file info', () => { 482 | const path = randomRootPath(); 483 | const file1 = 'file1.txt'; 484 | 485 | 486 | it('should retrieve extra properties when requested', async () => { 487 | await client.touchFolder(path); 488 | 489 | await client.put(`${path}/${file1}`, ''); 490 | 491 | let folderDetails = await client.getFolderFileDetails(path, [ 492 | createOwnCloudFileDetailProperty('fileid', true), 493 | createOwnCloudFileDetailProperty('size', true), 494 | createOwnCloudFileDetailProperty('owner-id'), 495 | createNextCloudFileDetailProperty('has-preview', true), 496 | createFileDetailProperty('http://doesnt/exist', 'de', 'test', false), 497 | createFileDetailProperty('http://doesnt/exist', 'de', 'test2', false, 42), 498 | createFileDetailProperty('http://doesnt/exist', 'de', 'test3', true), 499 | createFileDetailProperty('http://doesnt/exist', 'de', 'test4', true, 37), 500 | ]); 501 | 502 | folderDetails = folderDetails.filter(data => data.type === 'file'); 503 | 504 | const fileDetails = folderDetails[0]; 505 | expect(fileDetails.extraProperties['owner-id']).toBe('nextcloud'); 506 | expect(fileDetails.extraProperties['has-preview']).toBe(false); 507 | expect(fileDetails.extraProperties['test']).toBeUndefined(); 508 | expect(fileDetails.extraProperties['test2']).toBe(42); 509 | expect(fileDetails.extraProperties['test3']).toBeUndefined(); 510 | expect(fileDetails.extraProperties['test4']).toBe(37); 511 | expect(fileDetails.extraProperties['test999']).toBeUndefined(); 512 | 513 | await client.remove(path); 514 | }); 515 | }); 516 | 517 | describe('activity', () => { 518 | const folder1 = randomRootPath(); 519 | const folder2 = folder1 + randomRootPath(); 520 | const file1 = 'file1.txt'; 521 | const file2 = 'file2.txt'; 522 | 523 | beforeEach(async () => { 524 | await client.touchFolder(folder1); 525 | await client.touchFolder(folder2); 526 | await client.put(`${folder1}/${file1}`, ''); 527 | 528 | // Create activity 529 | await client.move(`${folder1}/${file1}`, `${folder2}/${file1}`); 530 | await client.move(`${folder2}/${file1}`, `${folder1}/${file1}`); 531 | await client.rename(`${folder1}/${file1}`, file2); 532 | await client.rename(`${folder1}/${file2}`, file1); 533 | }); 534 | 535 | afterEach(async () => { 536 | await client.remove(folder1); 537 | }); 538 | 539 | it('should retrieve the activity information of a file', async () => { 540 | let folderDetails = await client.getFolderFileDetails(folder1, [ 541 | createOwnCloudFileDetailProperty('fileid', true), 542 | ]); 543 | folderDetails = folderDetails.filter(data => data.type === 'file'); 544 | 545 | const fileDetails = folderDetails[0]; 546 | expect(fileDetails.extraProperties['fileid']).toBeDefined(); 547 | 548 | const fileId = fileDetails.extraProperties['fileid'] as number; 549 | const allActivities = await client.activities.get(fileId); 550 | expect(allActivities.length).toBe(5); 551 | 552 | const activity = allActivities.filter(activity => activity.type === 'file_created')[0]; 553 | 554 | expect(activity.user).toBe('nextcloud'); 555 | 556 | // data is the same regardless of sort direction. 557 | const ascActivities = await client.activities.get(fileId, 'asc'); 558 | expect(ascActivities.length).toBe(5); 559 | expect(ascActivities.length).toBe(allActivities.length); 560 | for (let ascIdx = 0; ascIdx < ascActivities.length; ascIdx++) { 561 | const allIdx = (ascActivities.length - 1) - ascIdx; 562 | 563 | expect(ascActivities[ascIdx].activityId).toBe(allActivities[allIdx].activityId); 564 | } 565 | 566 | // limit returns the expected amount. 567 | const threeAscActivities = await client.activities.get(fileId, 'asc', 3); 568 | expect(threeAscActivities.length).toBe(3); 569 | for (let idx = 0; idx < threeAscActivities.length; idx++) { 570 | expect(threeAscActivities[idx].activityId).toBe(ascActivities[idx].activityId); 571 | } 572 | 573 | // limited amount from different sort matches up. 574 | const sinceAscIdx = 1; 575 | const twoAscSinceActivities = await client.activities.get(fileId, 'asc', 2, ascActivities[sinceAscIdx].activityId); 576 | for (let twoAscIdx = 0; twoAscIdx < twoAscSinceActivities.length; twoAscIdx++) { 577 | const ascIdx = twoAscIdx + (sinceAscIdx + 1); 578 | expect(twoAscSinceActivities[twoAscIdx].activityId).toBe(ascActivities[ascIdx].activityId); 579 | } 580 | 581 | // since will return results AFTER the supplied id. Limit is maximum. 582 | const sinceAllIdx = 3; 583 | const oneAscSinceActivities = await client.activities.get(fileId, 'desc', 2, allActivities[sinceAllIdx].activityId); 584 | expect(oneAscSinceActivities.length).toBe(1); 585 | expect(oneAscSinceActivities[0].activityId).toBe(allActivities[sinceAllIdx + 1].activityId); 586 | 587 | // non-existing/invalid activityIds shouldn't throw an error but return null. 588 | let errorWhenRequestingWithInvalidActivityId = false; 589 | await client.activities.get(-5) 590 | .catch(error => { 591 | errorWhenRequestingWithInvalidActivityId = true; 592 | expect(error).toBeInstanceOf(OcsError); 593 | expect(error.statusCode).toBe(304); 594 | }); 595 | expect(errorWhenRequestingWithInvalidActivityId).toBeTruthy(); 596 | }); 597 | }); 598 | 599 | describe('user info', () => { 600 | const userId = 'nextcloud'; 601 | const invalidUserId = 'nextcloud2'; 602 | 603 | it('should retrieve user information', async () => { 604 | const user = await client.users.get(userId); 605 | 606 | expect(user).toBeDefined(); 607 | expect(user).not.toBeNull(); 608 | 609 | expect(user.enabled).toBeTruthy(); 610 | }); 611 | 612 | it('should get a null value when requesting a non-existing user', async () => { 613 | await expect(client.users.get(invalidUserId)).rejects.toBeInstanceOf(OcsError); 614 | }); 615 | }); 616 | 617 | describe('common function', () => { 618 | const userId = 'nextcloud'; 619 | const path = randomRootPath(); 620 | const filePath = `${path}${path}`; 621 | const notExistingFilePath = join(path, 'not_existing_file.txt'); 622 | const notExistingFullPath = join(randomRootPath(), 'not_existing_file.txt'); 623 | const string = 'Dummy content'; 624 | 625 | it('should retrieve the creator of a path', async () => { 626 | await client.touchFolder(path); 627 | expect(await client.exists(path)).toBeTruthy(); 628 | 629 | await client.put(filePath, string); 630 | 631 | await expect(client.getCreatorByPath(path)).resolves.toBe(userId); 632 | await expect(client.getCreatorByPath(filePath)).resolves.toBe(userId); 633 | await expect(client.getCreatorByPath(notExistingFilePath)).rejects.toBeInstanceOf(Error); 634 | await expect(client.getCreatorByPath(notExistingFullPath)).rejects.toBeInstanceOf(Error); 635 | 636 | await client.remove(path); 637 | }); 638 | }); 639 | 640 | describe('OCS commands with expected users and groups', () => { 641 | const numTestUsers = 2; 642 | const expectedUsers: OcsNewUser[] = []; 643 | expectedUsers.push({ 644 | userid: 'nextcloud', 645 | password: 'nextcloud', 646 | displayName: 'nextcloud', 647 | email: 'admin@nextcloud-link.test' 648 | }); 649 | 650 | for (let i = 1; i <= numTestUsers; i++) { 651 | expectedUsers.push({ 652 | userid: `test_user${i}`, 653 | password: 'nextcloud', 654 | displayName: `Test User ${i}`, 655 | email: `test_user${i}@nextcloud-link.test` 656 | }); 657 | } 658 | 659 | const numTestGroups = 2; 660 | const expectedGroups: string[] = [ 661 | 'admin' 662 | ]; 663 | for (let i = 1; i <= numTestGroups; i++) { 664 | expectedGroups.push(`group_test_${i}`); 665 | } 666 | 667 | beforeAll(async (done) => { 668 | try { 669 | await expectedUsers 670 | .filter(user => user.userid !== 'nextcloud') 671 | .forEach(async user => { 672 | await client.users.add(user); 673 | }); 674 | 675 | 676 | await expectedGroups 677 | .filter(groupId => groupId !== 'admin') 678 | .forEach(async groupId => { 679 | await client.groups.add(groupId); 680 | }); 681 | 682 | await new Promise(res => setTimeout(() => { 683 | // Added timeout because Nextcloud doesn't play nice with quick adds and reads. 684 | done(); 685 | }, 2000)); 686 | } catch (error) { 687 | console.error('Error during afterAll', error); 688 | done(); 689 | } 690 | }, 20000); 691 | 692 | afterAll(async () => { 693 | try { 694 | const userIds = await client.users.list(); 695 | if (userIds) { 696 | userIds 697 | .filter(userId => userId !== 'nextcloud') 698 | .forEach(async userId => { 699 | await client.users.delete(userId); 700 | }); 701 | } 702 | 703 | const groupIds = await client.groups.list(); 704 | if (groupIds) { 705 | groupIds 706 | .filter(groupId => groupId !== 'admin') 707 | .forEach(async groupId => { 708 | await client.groups.delete(groupId); 709 | }); 710 | } 711 | } catch (error) { 712 | console.error('Error during afterAll', error); 713 | } 714 | }, 20000); 715 | 716 | it('should add and remove users', async () => { 717 | const user: OcsNewUser = { 718 | userid: 'addUserTest', 719 | password: 'nextcloud' 720 | }; 721 | 722 | let userAdded = await client.users.add(user); 723 | let userDeleted = await client.users.delete(user.userid); 724 | 725 | expect(userAdded).toBeTruthy(); 726 | expect(userDeleted).toBeTruthy(); 727 | }, 5000); 728 | 729 | it('should list all users', async () => { 730 | const userIds = await client.users.list(); 731 | 732 | expect(userIds.length).toBe(expectedUsers.length); 733 | 734 | for (let i = 0; i < userIds.length; i++) { 735 | expect(userIds[i]).toBe(expectedUsers[i].userid); 736 | } 737 | }, 10000); 738 | 739 | it('should get data of a single user', async () => { 740 | const expectedUser = expectedUsers[1]; 741 | 742 | const user = await client.users.get(expectedUser.userid); 743 | 744 | expect(user.displayname).toBe(expectedUser.displayName); 745 | expect(user.enabled).toBeTruthy(); 746 | }); 747 | 748 | it('should manage a user\'s groups', async () => { 749 | const userId = expectedUsers[1].userid; 750 | const groupId = expectedGroups[1]; 751 | 752 | const addedToGroup = await client.users.addToGroup(userId, groupId); 753 | const groups = await client.users.getGroups(userId); 754 | const removedFromGroup = await client.users.removeFromGroup(userId, groupId); 755 | 756 | expect(addedToGroup).toBeTruthy(); 757 | expect(removedFromGroup).toBeTruthy(); 758 | expect(groups[0]).toBe(groupId); 759 | }, 10000); 760 | 761 | it('should edit a user', async () => { 762 | const expectedUser = expectedUsers[1]; 763 | const editedDisplayName = 'Edited displayname'; 764 | 765 | await client.users.edit(expectedUser.userid, 'displayname', editedDisplayName); 766 | 767 | const user = await client.users.get(expectedUser.userid); 768 | expect(user.id).toBe(expectedUser.userid); 769 | expect(user.displayname).toBe(editedDisplayName); 770 | 771 | await client.users.edit(expectedUser.userid, 'displayname', expectedUser.displayName); 772 | }, 13000); 773 | 774 | it('should be able to resend the welcome email', async () => { 775 | const expectedUser = expectedUsers[1]; 776 | 777 | const success = await client.users.resendWelcomeEmail(expectedUser.userid); 778 | expect(success).toBeTruthy(); 779 | }); 780 | 781 | it('should be able to change a user\'s enabled state', async () => { 782 | const userId = expectedUsers[1].userid; 783 | 784 | expect(await client.users.setEnabled(userId, false)).toBeTruthy(); 785 | const user = await client.users.get(userId); 786 | expect(await client.users.setEnabled(userId, true)).toBeTruthy(); 787 | expect(user.enabled).toBe(false); 788 | }, 10000); 789 | 790 | it('should be able to change a user\'s subAdmin rights', async () => { 791 | const userId = expectedUsers[1].userid; 792 | const groupId = expectedGroups[1]; 793 | 794 | const addedToGroup = await client.users.addSubAdminToGroup(userId, groupId); 795 | const subAdmins = await client.users.getSubAdminGroups(userId); 796 | const removedFromGroup = await client.users.removeSubAdminFromGroup(userId, groupId); 797 | 798 | expect(addedToGroup).toBeTruthy(); 799 | expect(removedFromGroup).toBeTruthy(); 800 | expect(subAdmins).toHaveLength(1); 801 | expect(subAdmins[0]).toBe(groupId); 802 | }, 10000); 803 | 804 | it('should list all groups', async () => { 805 | const groupIds = await client.groups.list(); 806 | 807 | expect(groupIds.length).toBe(expectedGroups.length); 808 | 809 | for (let i = 0; i < groupIds.length; i++) { 810 | expect(groupIds[i]).toBe(expectedGroups[i]); 811 | } 812 | }); 813 | 814 | it('should add and remove groups', async () => { 815 | const groupName = 'addGroupTest'; 816 | 817 | const added = await client.groups.add(groupName); 818 | const groupIds = await client.groups.list(); 819 | const removed = await client.groups.delete(groupName); 820 | 821 | expect(added).toBeTruthy(); 822 | expect(removed).toBeTruthy(); 823 | expect(groupIds).toContain(groupName); 824 | }); 825 | 826 | it('should list the users of a group', async (done) => { 827 | const groupName = expectedGroups[1]; 828 | 829 | await expectedUsers.forEach(async user => { 830 | await client.users.addToGroup(user.userid, groupName); 831 | }); 832 | 833 | await new Promise(res => setTimeout(async () => { 834 | const users = await client.groups.getUsers(groupName); 835 | 836 | await expectedUsers.forEach(async user => { 837 | await client.users.removeFromGroup(user.userid, groupName); 838 | }); 839 | 840 | // Added timeout because Nextcloud doesn't play nice with quick adds and reads. 841 | await new Promise(res => setTimeout(async () => { 842 | const users2 = await client.groups.getUsers(groupName); 843 | 844 | expect(users).toHaveLength(expectedUsers.length); 845 | expect(users2).toHaveLength(0); 846 | 847 | done(); 848 | }, 1000)); 849 | }, 1000)); 850 | }, 10000); 851 | 852 | it('should list the sub-admins of a group', async (done) => { 853 | const groupName = expectedGroups[1]; 854 | const added = {}; 855 | const removed = {}; 856 | 857 | await expectedUsers.forEach(async user => { 858 | const success = await client.users.addSubAdminToGroup(user.userid, groupName); 859 | added[user.userid] = success; 860 | }); 861 | 862 | await new Promise(res => setTimeout(async () => { 863 | const usersAfterAdd = await client.groups.getSubAdmins(groupName); 864 | 865 | await expectedUsers.forEach(async user => { 866 | const success = await client.users.removeSubAdminFromGroup(user.userid, groupName); 867 | removed[user.userid] = success; 868 | }); 869 | 870 | // Added timeout because Nextcloud doesn't play nice with quick adds and reads. 871 | await new Promise(res => setTimeout(async () => { 872 | const usersAfterRemove = await client.groups.getSubAdmins(groupName); 873 | 874 | expect(usersAfterAdd).toHaveLength(expectedUsers.length); 875 | expect(usersAfterRemove).toHaveLength(0); 876 | 877 | expectedUsers.forEach(user => { 878 | expect(added[user.userid]).toBeTruthy(); 879 | expect(removed[user.userid]).toBeTruthy(); 880 | }); 881 | 882 | done(); 883 | }, 1000)); 884 | }, 1000)); 885 | }, 10000); 886 | 887 | describe('requires files and folders', () => { 888 | const folder1 = randomRootPath(); 889 | const file1 = 'file1.txt'; 890 | const filePath = `${folder1}/${file1}`; 891 | 892 | beforeEach(async () => { 893 | try { 894 | await client.touchFolder(folder1); 895 | await client.put(`${folder1}/${file1}`, ''); 896 | } catch (error) { 897 | console.error('Error during file/folder beforeAll', error); 898 | } 899 | }); 900 | 901 | it('should be unable to retrieve acitivies of other users', async () => { 902 | const expectedUser = expectedUsers[1]; 903 | 904 | const otherClient = client.as(expectedUser.userid, expectedUser.password); 905 | 906 | let folderDetails = await client.getFolderFileDetails(folder1, [ 907 | createOwnCloudFileDetailProperty('fileid', true), 908 | ]); 909 | folderDetails = folderDetails.filter(data => data.type === 'file'); 910 | 911 | const fileDetails = folderDetails[0]; 912 | const fileId = fileDetails.extraProperties['fileid'] as number; 913 | 914 | const clientActivities = await client.activities.get(fileId); 915 | 916 | let errorWhenRequestingOtherUserActivities = false; 917 | await otherClient.activities.get(fileId) 918 | .catch(error => { 919 | errorWhenRequestingOtherUserActivities = true; 920 | expect(error).toBeInstanceOf(OcsError); 921 | expect(error.statusCode).toBe(304); 922 | }); 923 | 924 | expect(clientActivities).toHaveLength(1); 925 | expect(errorWhenRequestingOtherUserActivities).toBeTruthy(); 926 | }); 927 | 928 | describe('sharing API', () => { 929 | const password1 = 'as90123490j09jdsad'; 930 | const password2 = 'd90jk0324j0ds9a9ad'; 931 | 932 | it('should get a list of all shares', async () => { 933 | const expectedUser = expectedUsers[1]; 934 | const expectedGroup = expectedGroups[1]; 935 | await client.shares.add(folder1, OcsShareType.publicLink, '', OcsSharePermissions.read, password1); 936 | await client.shares.add(folder1, OcsShareType.user, expectedUser.userid, OcsSharePermissions.all); 937 | await client.shares.add(folder1, OcsShareType.group, expectedGroup, OcsSharePermissions.read | OcsSharePermissions.delete); 938 | 939 | const shares = await client.shares.list(); 940 | 941 | expect(shares).toHaveLength(3); 942 | }); 943 | 944 | it('should create a new share', async () => { 945 | const expectedGroup = expectedGroups[1]; 946 | const shareType = OcsShareType.group; 947 | 948 | const addedShare = await client.shares.add( 949 | folder1, 950 | shareType, 951 | expectedGroup, 952 | OcsSharePermissions.create | OcsSharePermissions.delete | OcsSharePermissions.share 953 | ); 954 | 955 | expect(addedShare).toBeDefined(); 956 | expect(addedShare.permissions & OcsSharePermissions.delete).toBe(OcsSharePermissions.delete); 957 | expect(addedShare.permissions & OcsSharePermissions.update).not.toBe(OcsSharePermissions.update); 958 | expect(addedShare.path).toBe(folder1); 959 | expect(addedShare.shareType).toBe(shareType); 960 | }); 961 | 962 | it('should get shares for a specific file or folder', async () => { 963 | const nonExistingFolder = '/nonExistingFolder'; 964 | const expectedUser = expectedUsers[1]; 965 | const expectedGroup = expectedGroups[1]; 966 | await client.shares.add(folder1, OcsShareType.publicLink, '', OcsSharePermissions.read, password1); 967 | await client.shares.add(filePath, OcsShareType.user, expectedUser.userid, OcsSharePermissions.all); 968 | await client.shares.add(filePath, OcsShareType.group, expectedGroup, OcsSharePermissions.read | OcsSharePermissions.delete); 969 | 970 | const shares = await client.shares.list(folder1, false, true); 971 | expect(shares).toHaveLength(2); 972 | 973 | const fileShares = await client.shares.list(filePath); 974 | expect(fileShares).toHaveLength(2); 975 | 976 | let errorWhenRequestingNonExistingFolder = false; 977 | await client.shares.list(nonExistingFolder, false, true) 978 | .catch(error => { 979 | errorWhenRequestingNonExistingFolder = true; 980 | expect(error).toBeInstanceOf(OcsError); 981 | expect(error.statusCode).toBe(404); 982 | }); 983 | expect(errorWhenRequestingNonExistingFolder).toBeTruthy(); 984 | 985 | let errorWhenRequestingSubFilesFromFile = false; 986 | await client.shares.list(filePath, false, true) 987 | .catch(error => { 988 | errorWhenRequestingSubFilesFromFile = true; 989 | expect(error).toBeInstanceOf(OcsError); 990 | expect(error.statusCode).toBe(400); 991 | }); 992 | expect(errorWhenRequestingSubFilesFromFile).toBeTruthy(); 993 | }); 994 | 995 | it('should get information about a known share', async () => { 996 | const addedShare = await client.shares.add(folder1, OcsShareType.publicLink, '', OcsSharePermissions.read, password1); 997 | const share = await client.shares.get(addedShare.id); 998 | 999 | expect(share.id).toBe(addedShare.id); 1000 | }); 1001 | 1002 | it('should delete a share', async () => { 1003 | const invalidShareId = -1; 1004 | const addedShare = await client.shares.add(folder1, OcsShareType.publicLink, '', OcsSharePermissions.read, password1); 1005 | const shareDeleted = await client.shares.delete(addedShare.id); 1006 | 1007 | expect(shareDeleted).toBeTruthy(); 1008 | 1009 | let errorWhenDeletingInvalidShareId = false; 1010 | await client.shares.delete(invalidShareId) 1011 | .catch(error => { 1012 | errorWhenDeletingInvalidShareId = true; 1013 | expect(error).toBeInstanceOf(OcsError); 1014 | expect(error.statusCode).toBe(404); 1015 | }); 1016 | expect(errorWhenDeletingInvalidShareId).toBeTruthy(); 1017 | }); 1018 | 1019 | it('should edit a share', async () => { 1020 | const expectedGroup = expectedGroups[1]; 1021 | const tempDate = new Date(); 1022 | const permissions1 = OcsSharePermissions.all; 1023 | const date1 = `${tempDate.getFullYear() + 1}-06-24`; 1024 | const note1 = 'This is the note'; 1025 | const publicSharePermissions1 = OcsSharePermissions.read | OcsSharePermissions.share | OcsSharePermissions.update | OcsSharePermissions.create | OcsSharePermissions.delete; 1026 | 1027 | const groupShare = await client.shares.add(folder1, OcsShareType.group, expectedGroup, OcsSharePermissions.delete); 1028 | const publicShare = await client.shares.add(folder1, OcsShareType.publicLink, '', OcsSharePermissions.read, password1); 1029 | 1030 | const permissionsUpdated = await client.shares.edit.permissions(groupShare.id, permissions1); 1031 | const expireDateUpdated = await client.shares.edit.expireDate(groupShare.id, date1); 1032 | const noteUpdated = await client.shares.edit.note(groupShare.id, note1); 1033 | 1034 | // Passwords are only on public links 1035 | const passwordUpdated = await client.shares.edit.password(publicShare.id, password2); 1036 | const publicUploadUpdated = await client.shares.edit.publicUpload(publicShare.id, true); 1037 | 1038 | expect(permissionsUpdated.permissions).toBe(permissions1); 1039 | expect(expireDateUpdated.expiration).toBe(`${date1} 00:00:00`); 1040 | expect(noteUpdated.note).toBe(note1); 1041 | expect(passwordUpdated.password).not.toBe(publicShare.password); 1042 | expect(publicShare.permissions).toBe(OcsSharePermissions.read | OcsSharePermissions.share); 1043 | expect(publicUploadUpdated.permissions).toBe(publicSharePermissions1); 1044 | }); 1045 | }); 1046 | }); 1047 | }); 1048 | }); 1049 | 1050 | function randomRootPath(): string { 1051 | return `/${Math.floor(Math.random() * 1000000000)}`; 1052 | } 1053 | 1054 | function getReadStream(string): Stream.Readable { 1055 | let readStream = new Stream.Readable(); 1056 | 1057 | readStream._read = () => {}; 1058 | 1059 | readStream.push(string); 1060 | readStream.push(null); 1061 | 1062 | return readStream; 1063 | } 1064 | 1065 | function getWriteStream(): Stream.Writable { 1066 | let writeStream = new Stream.Writable(); 1067 | 1068 | writeStream._write = (chunk, _, done) => { 1069 | writeStream.emit('testchunk', chunk); 1070 | writeStream.emit('close'); 1071 | done(); 1072 | }; 1073 | 1074 | return writeStream; 1075 | } 1076 | --------------------------------------------------------------------------------