{"id":15051433,"url":"https://github.com/tentwentyfour/nextcloud-link","last_synced_at":"2025-08-21T02:30:55.076Z","repository":{"id":41300829,"uuid":"129097385","full_name":"tentwentyfour/nextcloud-link","owner":"tentwentyfour","description":"Javascript/Typescript client that communicates with Nextcloud's WebDAV and OCS APIs","archived":false,"fork":false,"pushed_at":"2023-12-06T10:40:36.000Z","size":3846,"stargazers_count":57,"open_issues_count":13,"forks_count":7,"subscribers_count":13,"default_branch":"master","last_synced_at":"2024-12-18T00:27:05.263Z","etag":null,"topics":["javascript","nextcloud","nodejs","ocs","typescript","webdav"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tentwentyfour.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":["tentwentyfour"]}},"created_at":"2018-04-11T13:23:19.000Z","updated_at":"2024-09-03T13:10:01.000Z","dependencies_parsed_at":"2023-12-06T11:44:29.831Z","dependency_job_id":null,"html_url":"https://github.com/tentwentyfour/nextcloud-link","commit_stats":{"total_commits":163,"total_committers":13,"mean_commits":"12.538461538461538","dds":0.6503067484662577,"last_synced_commit":"8774d9885fb021f40c2827edc387ae0010ebc5ab"},"previous_names":[],"tags_count":37,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tentwentyfour%2Fnextcloud-link","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tentwentyfour%2Fnextcloud-link/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tentwentyfour%2Fnextcloud-link/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tentwentyfour%2Fnextcloud-link/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tentwentyfour","download_url":"https://codeload.github.com/tentwentyfour/nextcloud-link/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":230479864,"owners_count":18232630,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["javascript","nextcloud","nodejs","ocs","typescript","webdav"],"created_at":"2024-09-24T21:35:15.091Z","updated_at":"2024-12-19T18:14:17.172Z","avatar_url":"https://github.com/tentwentyfour.png","language":"TypeScript","funding_links":["https://github.com/sponsors/tentwentyfour"],"categories":[],"sub_categories":[],"readme":"# nextcloud-link ![npm](https://img.shields.io/npm/v/nextcloud-link?label=version)\n\n![](https://github.com/tentwentyfour/nextcloud-link/workflows/Node.js%20CI/badge.svg)\n![Snyk Vulnerabilities for GitHub Repo](https://img.shields.io/snyk/vulnerabilities/github/tentwentyfour/nextcloud-link)\n[![NPM Downloads](https://img.shields.io/npm/dt/nextcloud-link.svg?style=flat)](https://npmjs.org/package/nextcloud-link)\n[![Greenkeeper badge](https://badges.greenkeeper.io/tentwentyfour/nextcloud-link.svg)](https://greenkeeper.io/)\n![GitHub](https://img.shields.io/github/license/tentwentyfour/nextcloud-link?color=blue)\n![Twitter Follow](https://img.shields.io/twitter/follow/1024Lu?label=Follow%20TenTwentyFour\u0026style=social)\n\n\u003e Node.js client to interact with [Nextcloud](https://nextcloud.com), developed with :hearts: by [TenTwentyFour](https://tentwentyfour.lu).\n\n![tiny persons handling files in a huge directory](./cloud.png \"Directory\")\n\n## Table of Contents\n\n- [Getting Started](#getting-started)\n- [Features](#features)\n- [Interface](#interface)\n  - [Core](#core)\n  - [Activities](#activities)\n  - [Users](#users)\n  - [Groups](#groups)\n  - [Shares](#shares)\n  - [Groupfolders](#groupfolders)\n- [Exceptions](#exceptions)\n- [Types](#types)\n- [Helpers](#helpers)\n- [Definitions](#definitions)\n- [Contributing](#contributing)\n\n## Getting started\n\nIf you're not planning on contributing code to the project, you can simply install `nextcloud-link` to your project by running:\n\n`npm install --save nextcloud-link`\n\n### Quick-Start\n\nEstablishing a connection from your ECMA- or TypeScript project to a Nextcloud instance can be done like this:\n\n```TypeScript\nimport NextcloudClient from 'nextcloud-link';\n\nconst client = new NextcloudClient({\n  \"url\":      \"https://my.nextcloud.com\",\n  \"password\": \"useSomeBetterPassphraseThanThis\",\n  \"username\": \"cloudrider\",\n});\n```\n\nOnce 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:\n\n```TypeScript\nwhile (true) {\n  if (await client.checkConnectivity()) {\n    return;\n  }\n\n  await new Promise(resolve =\u003e setTimeout(resolve, 5000));\n}\n```\n\nIn 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.\n\nFinally, use any of the methods described below to interact with your Nextcloud instance:\n\n```TypeScript\nconst uploader = await client.getCreatorByPath('/Nextcloud.png');\n```\n\n## Features\n\n- :link: Interacts with Nextcloud instances via the WebDAV protocol\n- :rocket: Allows the use of streams for file transfer\n- :pray: Asserts Nextcloud connectivity before attempting any requests\n- :tada: OCS methods for groups, users, shares, activity, and groupfolders\n\n## Interface\n\n### Core\n\nThe following methods are available on `client`:\n\n`configureWebdavConnection(options: ConnectionOptions): void`\n\u003e 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.\n\n`checkConnectivity(): Promise\u003cboolean\u003e`\n\u003e Checks whether the connection to the configured WebDav endpoint succeeds. This does not throw, it consistently returns a Promise wrapping a boolean.\n\n`pipeStream(path: string, stream: Stream.Readable): Promise\u003cvoid\u003e`\n\u003e Deprecated, will be removed in version 2, use uploadFromStream\n\n`uploadFromStream(targetPath: string, stream: Stream.Readable): Promise\u003cvoid\u003e`\n\u003e Saves the data obtained through `stream` to the Nextcloud instance at `path`. Throws a `NotFoundError` if the requested path does not exist.\n\n`downloadToStream(sourcePath: string, stream: Stream.Readable): Promise\u003cvoid\u003e`\n\u003e 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.\n\n`as(username: string, password: string): NextcloudClient`\n\u003e 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.\n\n`createFolderHierarchy(path: string): Promise\u003cvoid\u003e`\n\u003e This is basically a recursive `mkdir`.\n\n`put(path: string, content: Webdav.ContentType): Promise\u003cvoid\u003e`\n\u003e This saves a Webdav.ContentType at `path`. Throws a `NotFoundError` if the path to the requested directory does not exist.\n\n`rename(fromFullPath: string, toFileName: string): Promise\u003cvoid\u003e`\n\u003e This allows to rename files or directories.\n\n`move(fromFullPath: string, toFullPath: string): Promise\u003cvoid\u003e`\n\u003e This allows to move files or entire directories.\n\n`getWriteStream(path: string): Promise\u003cWebdav.Stream\u003e`\n\u003e Gets a write stream to a remote Nextcloud `path`. Throws a `NotFoundError` if the path to the requested directory does not exist.\n\n`getReadStream(path: string): Promise\u003cWebdav.Stream\u003e`\n\u003e Gets a read stream to a remote Nextcloud `path`.\n\n`getFolderProperties(path: string, extraProperties?: FileDetailProperty[]): Promise\u003cFolderProperties\u003e`\n\u003e Retrieves properties for the folder. Use [extraProperties](#webdav-extraproperties) to request properties not returned by default.\n\n`touchFolder(path: string): Promise\u003cvoid\u003e`\n\u003e Smart `mkdir` implementation that doesn't complain if the folder at `path` already exists.\n\n`getFiles(path: string): Promise\u003cstring[]\u003e`\n\u003e List files in a directory.\n\n`getFolderFileDetails(path: string, extraProperties?: FileDetailProperty[]): Promise\u003cFileDetails[]\u003e`\n\u003e Same as `getFiles`, but returns more details instead of just file names. Use extraProperties to request properties not returned by default.\n\n`remove(path: string): Promise\u003cvoid\u003e`\n\u003e Removes file or directories. Does not complain if directories aren't empty.\n\n`exists(path: string): Promise\u003cboolean\u003e`\n\u003e Simple test that checks whether a file or directory exists. This indicates it in the return value, not by throwing exceptions.\n\n`get(path: string): Promise\u003cstring|Buffer\u003e`\n\u003e Gets a file as a string/Buffer.\n\n`getCreatorByPath(path: string): Promise\u003cstring\u003e`\n\u003e Gets the username of the user that created the file or folder.\n\n`getCreatorByFileId(fileId: number|string): Promise\u003cstring\u003e`\n\u003e Gets the username of the user that created the file or folder.\n\n### Activities\nThe following methods are available on `client.activities`\n\n`get(fileId: number|string, sort?: 'asc'|'desc', limit?: number, sinceActivityId?: number): Promise\u003cOcsActivity[]\u003e`\n\u003e Returns all activities belonging to a file or folder. Use the `limit` argument to override the server-default.\n\n### Users\nThe following methods are available on `client.users`:\n\n`removeSubAdminFromGroup(userId: string, groupId: string): Promise\u003cboolean\u003e`\n\u003e Remove a user as a Sub Admin from a group.\n\n`addSubAdminToGroup(userId: string, groupId: string): Promise\u003cboolean\u003e`\n\u003e Add a user as a Sub Admin to a group.\n\n`resendWelcomeEmail(userId: string): Promise\u003cboolean\u003e`\n\u003e Resend the Welcome email to a user.\n\n`removeFromGroup(userId: string, groupId: string): Promise\u003cboolean\u003e`\n\u003e Remove a user from a group.\n\n`getSubAdminGroups(userId: string): Promise\u003cstring[]\u003e`\n\u003e Gets a list of all the groups a user is a Sub Admin of.\n\n`setEnabled(userId: string, isEnabled: boolean): Promise\u003cboolean\u003e`\n\u003e Enables or disables a user.\n\n`addToGroup(userId: string, groupId: string): Promise\u003cboolean\u003e`\n\u003e Add a user to a group.\n\n`getGroups(userId: string): Promise\u003cstring[]\u003e`\n\u003e Gets a list of all the groups a user is a member of.\n\n`delete(userId: string): Promise\u003cboolean\u003e`\n\u003e Delete a user.\n\n`edit(userId: string, field: OcsEditUserField, value: string): Promise\u003cboolean\u003e`\n\u003e Edit a single field of a user.\n\n`list(search?: string, limit?: number, offset?: number): Promise\u003cstring[]\u003e`\n\u003e Gets a list of all users. Use the `limit` argument to override the server-default.\n\n`add(user: OcsNewUser): Promise\u003cboolean\u003e`\n\u003e Add a new user.\n\n`get(userId: string): Promise\u003cOcsUser\u003e`\n\u003e Gets the user information.\n\n### Groups\nThe following methods are available on `client.groups`:\n\n`getSubAdmins(groupId: string): Promise\u003cstring[]\u003e`\n\u003e Gets a list of all the users that are a Sub Admin of the group.\n\n`getUsers(groupId: string): Promise\u003cstring[]\u003e`\n\u003e Gets a list of all the users that are a member of the group.\n\n`delete(groupId: string): Promise\u003cboolean\u003e`\n\u003e Delete a group.\n\n`list(search?: string, limit?: number, offset?: number): Promise\u003cstring[]\u003e`\n\u003e Gets a list of all groups.\nUse the `limit` argument to override the server-default.\n\n`add(groupId: string): Promise\u003cboolean\u003e`\n\u003e Add a new group.\n\n### Shares\nThe following methods are available on `client.shares`:\n\n`delete(shareId: string| number):  Promise\u003cboolean\u003e`\n\u003e Delete a share.\n\n`list(path?: string, includeReshares?: boolean, showForSubFiles?: boolean): Promise\u003cOcsShare[]\u003e`\n\u003e 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.\n\n`add: (path: string, shareType: OcsShareType, shareWith?: string, permissions?: OcsSharePermissions, password?: string, publicUpload?: boolean): Promise\u003cOcsShare\u003e`\n\u003e 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.\n\n`get: (shareId: string|number): Promise\u003cOcsShare\u003e`\n\u003e Gets the share information.\n\n#### edit\nThe following methods are available on `client.shares.edit`:\n\n`permissions(shareId: string|number, permissions: OcsSharePermissions): Promise\u003cOcsShare\u003e`\n\u003e Change the permissions. Use `permissions` bit-wise to add several permissions.\n\n`password(shareId: string|number, password: string): Promise\u003cOcsShare\u003e`\n\u003e Change the password. Only `OcsShareType.publicLink` uses passwords.\n\n`publicUpload(shareId: string|number, isPublicUpload: boolean): Promise\u003cOcsShare\u003e`\n\u003e Enable / disable public upload for public shares.\n\n`expireDate(shareId: string|number, expireDate: string): Promise\u003cOcsShare\u003e`\n\u003e Add an expire date to the share. If the expire date is in the past, Nextcloud will remove the share.\n\n`note(shareId: string|number, note: string): Promise\u003cOcsShare\u003e`\n\u003e Add a note to the share.\n\n### Groupfolders\n\nTo be able to use `groupfolders` interface, the [groupfolders](https://github.com/nextcloud/groupfolders) app needs to be downloaded and activated in the Nextcloud settings.\nThe following methods are available on `client.groupfolders`:\n\n`getFolders: () =\u003e Promise\u003cOcsGroupfolder[]\u003e`\n\u003e Returns a list of all configured folders and their settings.\n\n`getFolder: (fid: number) =\u003e Promise\u003cOcsGroupfolder\u003e`\n\u003e Return a specific configured groupfolder and its settings, `null` if not found.\n\n`addFolder: (mountpoint: string) =\u003e Promise\u003cnumber\u003e`\n\u003e Create a new groupfolder with name `mountpoint` and returns its `id`.\n\n`removeFolder: (fid: number) =\u003e Promise\u003cboolean\u003e`\n\u003e Delete a groupfolder. Returns `true` if successful (even if the groupfolder didn't exist).\n\n`addGroup: (fid: number, gid: string) =\u003e Promise\u003cboolean\u003e`\n\u003e Give a group access to a groupfolder.\n\n`removeGroup: (fid: number, gid: string) =\u003e Promise\u003cboolean\u003e`\n\u003e Remove access from a group to a groupfolder.\n\n`setPermissions: (fid: number, gid: string, permissions: number) =\u003e Promise\u003cboolean\u003e`\n\u003e 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).\n\n`enableACL: (fid: number, enable: boolean) =\u003e Promise\u003cboolean\u003e`\n\u003e Enable/Disable groupfolder advanced permissions.\n\n`setManageACL: (fid: number, type: 'group' | 'user', id: string, manageACL: boolean) =\u003e Promise\u003cboolean\u003e`\n\u003e Grants/Removes a group or user the ability to manage a groupfolders' advanced permissions.\n\u003e `mappingId`: the id of the group/user to be granted/removed access to/from the groupfolder\n\u003e `mappingType`: 'group' or 'user'\n\u003e `manageAcl`: true to grants ability to manage a groupfolders' advanced permissions, false to remove\n\n`setQuota: (fid: number, quota: number) =\u003e Promise\u003cboolean\u003e`\n\u003e Set the `quota` for a groupfolder in bytes (use `-3` for unlimited).\n\n`renameFolder: (fid: number, mountpoint: string) =\u003e Promise\u003cboolean\u003e`\n\u003e Change the name of a groupfolder to `mountpoint`.\n\nNote: 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\".\n\n## Exceptions\n\n### NotFoundError\nError indicating that the requested resource doesn't exist, or that the path leading to it doesn't exist in the case of writes.\n\n### ForbiddenError\nError indicating that Nextcloud denied the request.\n\n### NextcloudError\nGeneric wrapper for the HTTP errors returned by Nextcloud.\n\n### OcsError\nErrors used by all OCS calls.\nIt will return the reason why a request failed as well as a status code if it is available.\n\n## Types\n### ConnectionOptions\n```javascript\ninterface  ConnectionOptions {\n  url:        string;\n  username?:  string;\n  password?:  string;\n}\n```\n\n### WebDAV\n```javascript\ninterface FileDetails {\n    creationDate?: Date;\n    lastModified:  Date;\n    href:          string;\n    name:          string;\n    size:          number;\n    isDirectory:   boolean;\n    isFile:        boolean;\n    type:          'directory' | 'file';\n}\n```\n\n### OCS\n```javascript\ninterface OcsActivity {\n  activityId:  number;\n  app:         string;\n  type:        string;\n  user:        string;\n  subject:     string;\n  subjectRich: [];\n  message:     string;\n  messageRich: [];\n  objectType:  string;\n  fileId:      number;\n  objectName:  string;\n  objects:     {};\n  link:        string;\n  icon:        string;\n  datetime:    Date;\n}\n\ninterface OcsUser {\n  id:          string;\n  enabled:     boolean;\n  lastLogin:   number;\n  email:       string;\n  displayname: string;\n  phone:       string;\n  address:     string;\n  website:     string;\n  twitter:     string;\n  groups:      string[];\n  language:    string;\n  locale:      string;\n}\n\ninterface OcsNewUser {\n  userid:       string;\n  password?:    string;\n  email?:       string;\n  displayName?: string;\n  groups?:      string[];\n  subadmin?:    string[];\n  quota?:       number;\n  language?:    string;\n}\n\ntype OcsEditUserField =\n  'password'    |\n  'email'       |\n  'displayname' |\n  'quota'       |\n  'phone'       |\n  'address'     |\n  'website'     |\n  'twitter'     |\n  'locale'      |\n  'language'    ;\n\nenum OcsShareType {\n  user                = 0,\n  group               = 1,\n  publicLink          = 3,\n  federatedCloudShare = 6,\n}\n\nenum OcsSharePermissions {\n  default = -1,\n  read    =  1,\n  update  =  2,\n  create  =  4,\n  delete  =  8,\n  share   = 16,\n  all     = 31,\n}\n\ninterface OcsShare {\n  id:                    number;\n  shareType:             OcsShareType;\n  shareTypeSystemName:   string;\n  ownerUserId:           string;\n  ownerDisplayName:      string;\n  permissions:           OcsSharePermissions;\n  permissionsText:       string;\n  sharedOn:              Date;\n  sharedOnTimestamp:     number;\n  parent:                string;\n  expiration:            Date;\n  token:                 string;\n  fileOwnerUserId:       string;\n  fileOwnerDisplayName:  string;\n  note:                  string;\n  label:                 string;\n  path:                  string;\n  itemType:              'file' | 'folder';\n  mimeType:              string;\n  storageId:             string;\n  storage:               number;\n  fileId:                number;\n  parentFileId:          number;\n  fileTarget:            string;\n  sharedWith:            string;\n  sharedWithDisplayName: string;\n  mailSend:              boolean;\n  hideDownload:          boolean;\n  password?:             string;\n  sendPasswordByTalk?:   boolean;\n  url?:                  string;\n}\n\ntype OcsEditShareField =\n  'permissions'     |\n  'password'        |\n  'expireDate'      |\n  'note'            ;\n\ninterface OcsGroupfolderManageRule {\n  type:        'group' | 'user'\n  id:          string;\n  displayname: string;\n}\n\ninterface OcsGroupfolder {\n  id:         number;\n  mountPoint: string;\n  groups:     Record\u003cstring, number\u003e;\n  quota:      number;\n  size:       number;\n  acl:        boolean;\n  manage?:    OcsGroupfolderManageRule[];\n}\n```\n\n## Helpers\n\n`createFileDetailProperty(namespace: string, namespaceShort: string, element: string, nativeType?: boolean, defaultValue?: any): FileDetailProperty`\n\u003e Creates a FileDetailProperty filled in with the supplied arguments, which can be used when using getFolderFileDetails.\n\n`createOwnCloudFileDetailProperty(element: string, nativeType?: boolean, defaultValue?: any): FileDetailProperty`\n\u003e Uses createFileDetailProperty to request an OwnCloud property.\n\n`createNextCloudFileDetailProperty(element:string, nativeType?: boolean, defaultValue?: any): FileDetailProperty`\n\u003e Uses createFileDetailProperty to request a Nextcloud property.\n\n## Definitions\n\n### fileId\n\nThis 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\non `extraProperties`.\n\n### WebDAV extraProperties\n\n`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.\n\nA simple example that requests the `fileId` of a directory on top of the standard properties returned by the `WebDAV` API would be:\n\n```typescript\nconst fileId = createOwnCloudFileDetailProperty('fileid', true);\nconst documentList = await client.getFolderFileDetails('/Documents', [fileId]);\nfor (const directory of documentList) {\n  const folderId = directory.extraProperties.fileid;\n}\n```\nWhich 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).\n\n### Sub Admin\n\nThis is a Nextcloud term used to describe a user that has administrator rights for a group.\n\n## Contributing\n\nRunning 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.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftentwentyfour%2Fnextcloud-link","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftentwentyfour%2Fnextcloud-link","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftentwentyfour%2Fnextcloud-link/lists"}