From b538f72004d15312383266fc4154f68cd12f4934 Mon Sep 17 00:00:00 2001 From: Philipp Kunz Date: Wed, 24 Apr 2024 18:02:21 +0200 Subject: [PATCH] fix(core): update --- license | 22 +++++++ package.json | 8 ++- pnpm-lock.yaml | 76 +++++++++++++++++------- qenv.yml | 2 +- test/test.ts | 30 ++++++++-- ts/00_commitinfo_data.ts | 2 +- ts/classes.exposeprovider.ts | 2 +- ts/classes.exposeprovider.webdav.ts | 89 ++++++++++++++++++++++++----- ts/classes.smartexpose.ts | 20 ++++++- ts/plugins.ts | 12 ++++ 10 files changed, 219 insertions(+), 44 deletions(-) create mode 100644 license diff --git a/license b/license new file mode 100644 index 0000000..2ed0db9 --- /dev/null +++ b/license @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014 Task Venture Capital GmbH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/package.json b/package.json index c170792..4fa7463 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,15 @@ "@types/node": "^20.8.7" }, "dependencies": { + "@push.rocks/smartdelay": "^3.0.5", + "@push.rocks/smartexpect": "^1.0.21", "@push.rocks/smartfile": "^11.0.14", + "@push.rocks/smartformat": "^1.0.3", "@push.rocks/smartjson": "^5.0.19", - "@push.rocks/smartwebdav": "^1.0.4", + "@push.rocks/smartpath": "^5.0.18", + "@push.rocks/smartrequest": "^2.0.22", + "@push.rocks/smartunique": "^3.0.8", + "@push.rocks/smartwebdav": "^1.1.2", "@push.rocks/taskbuffer": "^3.1.7" }, "repository": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1145eeb..81bb154 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,15 +5,33 @@ settings: excludeLinksFromLockfile: false dependencies: + '@push.rocks/smartdelay': + specifier: ^3.0.5 + version: 3.0.5 + '@push.rocks/smartexpect': + specifier: ^1.0.21 + version: 1.0.21 '@push.rocks/smartfile': specifier: ^11.0.14 version: 11.0.14 + '@push.rocks/smartformat': + specifier: ^1.0.3 + version: 1.0.3 '@push.rocks/smartjson': specifier: ^5.0.19 version: 5.0.19 + '@push.rocks/smartpath': + specifier: ^5.0.18 + version: 5.0.18 + '@push.rocks/smartrequest': + specifier: ^2.0.22 + version: 2.0.22 + '@push.rocks/smartunique': + specifier: ^3.0.8 + version: 3.0.8 '@push.rocks/smartwebdav': - specifier: ^1.0.4 - version: 1.0.4 + specifier: ^1.1.2 + version: 1.1.2 '@push.rocks/taskbuffer': specifier: ^3.1.7 version: 3.1.7 @@ -78,7 +96,7 @@ packages: '@push.rocks/smartmanifest': 2.0.2 '@push.rocks/smartmime': 1.0.6 '@push.rocks/smartopen': 2.0.0 - '@push.rocks/smartpath': 5.0.14 + '@push.rocks/smartpath': 5.0.18 '@push.rocks/smartpromise': 4.0.3 '@push.rocks/smartrequest': 2.0.22 '@push.rocks/smartrx': 3.0.7 @@ -387,7 +405,7 @@ packages: '@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartfile': 11.0.14 '@push.rocks/smartlog': 3.0.3 - '@push.rocks/smartpath': 5.0.14 + '@push.rocks/smartpath': 5.0.18 '@push.rocks/smartpromise': 4.0.3 typescript: 5.3.3 dev: true @@ -402,7 +420,7 @@ packages: '@push.rocks/smartfile': 11.0.14 '@push.rocks/smartlog': 3.0.3 '@push.rocks/smartlog-destination-local': 9.0.1 - '@push.rocks/smartpath': 5.0.14 + '@push.rocks/smartpath': 5.0.18 '@push.rocks/smartpromise': 4.0.3 '@push.rocks/smartspawn': 3.0.2 '@types/html-minifier': 4.0.5 @@ -610,7 +628,7 @@ packages: '@configvault.io/interfaces': 1.0.17 '@push.rocks/smartfile': 11.0.14 '@push.rocks/smartlog': 3.0.3 - '@push.rocks/smartpath': 5.0.16 + '@push.rocks/smartpath': 5.0.18 dev: true /@push.rocks/smartbrowser@2.0.6: @@ -688,7 +706,6 @@ packages: '@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartpromise': 4.0.3 fast-deep-equal: 3.1.3 - dev: true /@push.rocks/smartfeed@1.0.11: resolution: {integrity: sha512-02uhXxQamgfBo3T12FsAdfyElnpoWuDUb08B2AE60DbIaukVx/7Mi17xwobApY1flNSr5StZDt8N8vxPhBhIXw==} @@ -710,7 +727,7 @@ packages: '@push.rocks/smarthash': 3.0.4 '@push.rocks/smartjson': 5.0.19 '@push.rocks/smartmime': 1.0.6 - '@push.rocks/smartpath': 5.0.14 + '@push.rocks/smartpath': 5.0.18 '@push.rocks/smartpromise': 4.0.3 '@push.rocks/smartrequest': 2.0.22 '@push.rocks/smartstream': 2.0.8 @@ -731,7 +748,7 @@ packages: '@push.rocks/smarthash': 3.0.4 '@push.rocks/smartjson': 5.0.19 '@push.rocks/smartmime': 1.0.6 - '@push.rocks/smartpath': 5.0.16 + '@push.rocks/smartpath': 5.0.18 '@push.rocks/smartpromise': 4.0.3 '@push.rocks/smartrequest': 2.0.22 '@push.rocks/smartstream': 3.0.34 @@ -742,6 +759,13 @@ packages: glob: 10.3.12 js-yaml: 4.1.0 + /@push.rocks/smartformat@1.0.3: + resolution: {integrity: sha512-NjJ6akOzUk1UWxezgkIhklY+1EpFfcEbbF+DwTl8FL7sXYLHHS7L2MafuZToG+Y3INq8ahwh0jF4J0ZX/EtkIA==} + dependencies: + pretty-bytes: 6.1.1 + pretty-ms: 9.0.0 + dev: false + /@push.rocks/smarthash@3.0.4: resolution: {integrity: sha512-HJ/fSx41jm0CvSaqMLa6b2nuNK5rHAqAeAq3dAB7Sq9BCPm2M0J5ZVDTzEAH8pS91XYniUiwuE0jwPERNn9hmw==} dependencies: @@ -824,12 +848,8 @@ packages: minimatch: 9.0.4 dev: true - /@push.rocks/smartpath@5.0.14: - resolution: {integrity: sha512-/FwGJ7+y1il6joO+bTc45X8z8h/UKUyZqNSvrKMA69zHFn8dl2552OApVcNphuAxhid4OUAOVSnJFVywOdRYyw==} - dev: true - - /@push.rocks/smartpath@5.0.16: - resolution: {integrity: sha512-xDOrxRAgniETNKga7yUJA3KJQQETXa3yuWpvfscPUbUcuatsdg6cVv1320yr7QWgJz0A1V4ww+MMnJI2HtNAtA==} + /@push.rocks/smartpath@5.0.18: + resolution: {integrity: sha512-kIyRTlOoeEth5b4Qp8KPUxNOGNdvhb2aD0hbHfF3oGTQ0xnDdgB1l03/4bIoapHG48OrTgh4uQ5tUorykgdOzw==} /@push.rocks/smartpdf@3.0.17: resolution: {integrity: sha512-oymxajeDnwMzg20Ru7GkpYHM4KtNyTrvxt+AwXD3EjzOaJnFtevszDpCdcCruBik8BcG2XiKJIyY5PprPALvaA==} @@ -837,7 +857,7 @@ packages: '@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartfile': 10.0.41 '@push.rocks/smartnetwork': 3.0.2 - '@push.rocks/smartpath': 5.0.14 + '@push.rocks/smartpath': 5.0.18 '@push.rocks/smartpromise': 4.0.3 '@push.rocks/smartpuppeteer': 2.0.2 '@push.rocks/smartunique': 3.0.8 @@ -992,11 +1012,11 @@ packages: /@push.rocks/smarturl@3.0.7: resolution: {integrity: sha512-nx4EWjQD9JeO7QVbOsxd1PFeDQYoSQOOOYCZ+r7QWXHLJG52iYzgvJDCQyX6p705HDkYMJWozW2ZzhR22qLKbw==} - /@push.rocks/smartwebdav@1.0.4: - resolution: {integrity: sha512-iU2xlAMOnx0s4zdSNCDIKnxpTZ8uoulf9b/cRh/+OcfyyyD9pmjaXyAldlpo144X604GTrXxcxkEPEPWndiVEA==} + /@push.rocks/smartwebdav@1.1.2: + resolution: {integrity: sha512-cfIFUbeo8DOBACS/4MaBpGX+xInnGVyOIg1BkkllhTe+XQcFxvqryo56d/UEEKX+BnpMdJm3LyHYEe/iJCLWPw==} dependencies: '@push.rocks/smartfile': 11.0.14 - '@push.rocks/smartpath': 5.0.16 + '@push.rocks/smartpath': 5.0.18 webdav: 5.5.0 dev: false @@ -2591,7 +2611,6 @@ packages: /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: true /fast-glob@3.3.2: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} @@ -3761,6 +3780,11 @@ packages: resolution: {integrity: sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw==} engines: {node: '>=12'} + /parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + dev: false + /parse5@6.0.1: resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} dev: true @@ -3855,6 +3879,11 @@ packages: find-up: 4.1.0 dev: true + /pretty-bytes@6.1.1: + resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} + engines: {node: ^14.13.1 || >=16.0.0} + dev: false + /pretty-ms@7.0.1: resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==} engines: {node: '>=10'} @@ -3868,6 +3897,13 @@ packages: dependencies: parse-ms: 3.0.0 + /pretty-ms@9.0.0: + resolution: {integrity: sha512-E9e9HJ9R9NasGOgPaPE8VMeiPKAyWR5jcFpNnwIejslIhWqdqOrb2wShBsncMPUb+BcCd2OPYfh7p2W6oemTng==} + engines: {node: '>=18'} + dependencies: + parse-ms: 4.0.0 + dev: false + /process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} dev: true diff --git a/qenv.yml b/qenv.yml index d8689d1..d4eaa52 100644 --- a/qenv.yml +++ b/qenv.yml @@ -2,4 +2,4 @@ required: - WEBDAV_SERVER_URL - WEBDAV_SERVER_TOKEN - WEBDAV_SUB_PATH - - WEBDAV_EXPOSED_BASE_URL \ No newline at end of file + - EXPOSED_BASE_URL \ No newline at end of file diff --git a/test/test.ts b/test/test.ts index 245e039..fa4572e 100644 --- a/test/test.ts +++ b/test/test.ts @@ -1,5 +1,7 @@ import { expect, expectAsync, tap } from '@push.rocks/tapbundle'; import * as qenv from '@push.rocks/qenv'; +import * as smartfile from '@push.rocks/smartfile'; + const testQenv = new qenv.Qenv('./', './.nogit/'); import * as smartexpose from '../ts/index.js' @@ -10,15 +12,33 @@ tap.test('should create a valid instance of smartexpose using Webdav', async () testSmartexpose = await smartexpose.SmartExpose.createWithWebdav({ webdavCredentials: { serverUrl: await testQenv.getEnvVarOnDemand('WEBDAV_SERVER_URL'), - password: await testQenv.getEnvVarOnDemand('WEBDAV_PASSWORD'), + password: await testQenv.getEnvVarOnDemand('WEBDAV_SERVER_TOKEN'), }, - exposedBaseUrl: await testQenv.getEnvVarOnDemand('WEBDAV_EXPOSED_BASE_URL'), webdavSubPath: await testQenv.getEnvVarOnDemand('WEBDAV_SUB_PATH'), exposeOptions: { deleteAfterMillis: 30000, privateUrl: true, + exposedBaseUrl: await testQenv.getEnvVarOnDemand('EXPOSED_BASE_URL'), } - }) -} + }); + await testSmartexpose.start(); + expect(testSmartexpose).toBeInstanceOf(smartexpose.SmartExpose); +}); -tap.start() +tap.test('should expose a file', async () => { + await testSmartexpose.exposeFile({ + smartFile: await smartfile.SmartFile.fromString('okidoks', 'hi there', 'utf8'), + deleteAfterMillis: 10000, + privateUrl: true, + }); +}); + +tap.test('should delete the file', async (toolsArg) => { + await toolsArg.delayFor(11000); +}); + +tap.test('should stop the smartexpose', async () => { + await testSmartexpose.stop(); +}) + +export default tap.start(); diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index c32b46e..5e857f1 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartexpose', - version: '1.0.2', + version: '1.0.3', description: 'a package to expose things to the internet' } diff --git a/ts/classes.exposeprovider.ts b/ts/classes.exposeprovider.ts index 814fa19..eda7c3d 100644 --- a/ts/classes.exposeprovider.ts +++ b/ts/classes.exposeprovider.ts @@ -36,7 +36,7 @@ export abstract class ExposeProvider { id: string; status: 'deleted' | 'failed' | 'notfound'; }[]> - public abstract wipeFileById(idArg: string): Promise<{ + public abstract deleteFileById(idArg: string): Promise<{ id: string; status: 'deleted' | 'failed' | 'notfound'; }> diff --git a/ts/classes.exposeprovider.webdav.ts b/ts/classes.exposeprovider.webdav.ts index cf724c4..9856f19 100644 --- a/ts/classes.exposeprovider.webdav.ts +++ b/ts/classes.exposeprovider.webdav.ts @@ -1,12 +1,14 @@ import { ExposeProvider } from './classes.exposeprovider.js'; +import type { SmartExpose } from './classes.smartexpose.js'; import * as plugins from './plugins.js'; export interface IWebdavExposeProviderOptions { - webdavCredentials: plugins.smartwebdav.IWebdavClientOptions, - webdavSubPath: string, + webdavCredentials: plugins.smartwebdav.IWebdavClientOptions; + webdavSubPath: string; } export class WebDavExposeProvider extends ExposeProvider { + public smartExposeRef: SmartExpose; public webdavClient: plugins.smartwebdav.WebdavClient; public options: IWebdavExposeProviderOptions; @@ -15,40 +17,99 @@ export class WebDavExposeProvider extends ExposeProvider { this.options = optionsArg; } - public async houseKeeping (): Promise { + public async houseKeeping(): Promise { const directoryContents = await this.webdavClient.listDirectory(this.options.webdavSubPath); for (const fileStat of directoryContents) { // lets read the meta.json file if (fileStat.filename.endsWith('.json')) { - } } } - public async ensureBaseDir (): Promise { + public async getFilePathById(idArg: string): Promise { + return plugins.smartpath.join(this.options.webdavSubPath, idArg); + } + + public async ensureBaseDir(): Promise { await this.webdavClient.ensureDirectory(this.options.webdavSubPath); } - public async start (): Promise { + public async start(): Promise { this.webdavClient = new plugins.smartwebdav.WebdavClient(this.options.webdavCredentials); await this.ensureBaseDir(); await this.houseKeeping(); + this.smartExposeRef.taskmanager.addAndScheduleTask(new plugins.taskbuffer.Task({ + name: 'webdavHousekeeping', + taskFunction: async () => { + await this.houseKeeping(); + }, + }), '0 * * * * *') } - public async stop (): Promise { + public async stop(): Promise { // Nothing to do here } - public async exposeFile (optionsArg: { smartFile: plugins.smartfile.SmartFile; deleteAfterMillis?: number; privateUrl?: boolean; }): Promise<{ url: string; id: string; status: 'created' | 'updated'; }> { + public async exposeFile(optionsArg: { + smartFile: plugins.smartfile.SmartFile; + deleteAfterMillis?: number; + privateUrl?: boolean; + }): Promise<{ url: string; id: string; status: 'created' | 'updated' }> { await this.ensureBaseDir(); - + const fileId = plugins.smartunique.shortId(30); + console.log(`Expsing file under id: ${fileId}. (${ + plugins.smartformat.prettyBytes(optionsArg.smartFile.contents.length) + })`); + const webdavFilePath = await this.getFilePathById(fileId); + const fileToUpload = await plugins.smartfile.SmartFile.fromBuffer(webdavFilePath, optionsArg.smartFile.contents); + await this.webdavClient.uploadSmartFileArray([fileToUpload]); + console.log(`checking file presence: ${webdavFilePath}`); + const existsOnWebdav = await this.webdavClient.wdClient.exists(webdavFilePath); + const publicUrl = plugins.smartpath.join(this.smartExposeRef.options.exposedBaseUrl, webdavFilePath); + console.log(`cehcking for file at ${publicUrl}`) + const response = await plugins.smartrequest.getBinary(publicUrl); + plugins.smartexpect.expect(response.body).toEqual(fileToUpload.contents); + if (optionsArg.deleteAfterMillis) { + console.log(`Scheduling deletion of file with id: ${fileId} in ${optionsArg.deleteAfterMillis}ms...`); + plugins.smartdelay.delayFor(optionsArg.deleteAfterMillis).then(() => { + console.log(`Deleting file with id: ${fileId}...`); + this.deleteFileById(fileId); + console.log(`Deleted file with id: ${fileId}`); + }); + } + return { + url: publicUrl, + id: fileId, + status: 'created', + }; } - public async wipeAll (): Promise<{ id: string; status: 'deleted' | 'failed' | 'notfound'; }[]> { - + public async wipeAll(): Promise<{ id: string; status: 'deleted' | 'failed' | 'notfound' }[]> { + const directoryContents = await this.webdavClient.listDirectory(this.options.webdavSubPath); + await this.webdavClient.ensureEmptyDirectory(this.options.webdavSubPath); + return directoryContents.map((contentArg) => { + return { + id: contentArg.filename, + status: 'deleted', + }; + }); } - public async wipeFileById (idArg: string): Promise<{ id: string; status: 'deleted' | 'failed' | 'notfound'; }> { - + public async deleteFileById( + idArg: string + ): Promise<{ id: string; status: 'deleted' | 'failed' | 'notfound' }> { + const filePath = await this.getFilePathById(idArg); + if (!this.webdavClient.wdClient.exists(filePath)) { + return { + id: idArg, + status: 'notfound', + }; + } + + await this.webdavClient.deleteFile(filePath); + return { + id: idArg, + status: 'deleted', + }; } -} \ No newline at end of file +} diff --git a/ts/classes.smartexpose.ts b/ts/classes.smartexpose.ts index 91f1a20..2f18b53 100644 --- a/ts/classes.smartexpose.ts +++ b/ts/classes.smartexpose.ts @@ -5,6 +5,7 @@ import * as plugins from './plugins.js'; export interface ISmartExposeOptions { deleteAfterMillis?: number, privateUrl?: boolean, + exposedBaseUrl: string, } export class SmartExpose { @@ -12,7 +13,6 @@ export class SmartExpose { public static createWithWebdav(optionsArg: { webdavCredentials: plugins.smartwebdav.IWebdavClientOptions, webdavSubPath: string, - exposedBaseUrl: string, exposeOptions: ISmartExposeOptions, }) { const provider = new WebDavExposeProvider({ @@ -20,14 +20,32 @@ export class SmartExpose { webdavSubPath: optionsArg.webdavSubPath, }); const smartexposeInstance = new SmartExpose(provider, optionsArg.exposeOptions); + provider.smartExposeRef = smartexposeInstance; return smartexposeInstance; } // INSTANCE public taskmanager: plugins.taskbuffer.TaskManager; + public provider: ExposeProvider; public options: ISmartExposeOptions; constructor(provider: ExposeProvider, optionsArg: ISmartExposeOptions) { + this.provider = provider; this.options = optionsArg; } + + public async start() { + this.taskmanager = new plugins.taskbuffer.TaskManager(); + await this.provider.start(); + this.taskmanager.start(); + } + + public async stop() { + await this.provider.stop(); + this.taskmanager.stop(); + } + + public async exposeFile(optionsArg: Parameters[0]) { + return this.provider.exposeFile(optionsArg); + } } diff --git a/ts/plugins.ts b/ts/plugins.ts index f80ae9d..7252a41 100644 --- a/ts/plugins.ts +++ b/ts/plugins.ts @@ -1,12 +1,24 @@ // @push.rocks scope +import * as smartdelay from '@push.rocks/smartdelay'; +import * as smartexpect from '@push.rocks/smartexpect'; import * as smartfile from '@push.rocks/smartfile'; +import * as smartformat from '@push.rocks/smartformat'; import * as smartjson from '@push.rocks/smartjson'; +import * as smartpath from '@push.rocks/smartpath'; +import * as smartrequest from '@push.rocks/smartrequest'; +import * as smartunique from '@push.rocks/smartunique'; import * as smartwebdav from '@push.rocks/smartwebdav'; import * as taskbuffer from '@push.rocks/taskbuffer'; export { + smartdelay, + smartexpect, smartfile, + smartformat, smartjson, + smartpath, + smartrequest, + smartunique, smartwebdav, taskbuffer, }