OpenMarshal/npm-WebDAV-Server

View on GitHub
src/manager/v2/fileSystem/FileSystem.ts

Summary

Maintainability
F
6 days
Test Coverage
// tslint:disable:max-file-line-count max-line-length
import {
    PrivilegeManagerInfo,
    AvailableLocksInfo,
    CopyInfo,
    CreateInfo,
    CreationDateInfo,
    DeleteInfo,
    DisplayNameInfo,
    ETagInfo,
    LastModifiedDateInfo,
    LockManagerInfo,
    MimeTypeInfo,
    MoveInfo,
    OpenReadStreamInfo,
    OpenWriteStreamInfo,
    PropertyManagerInfo,
    ReadDirInfo,
    RenameInfo,
    SizeInfo,
    TypeInfo
} from './ContextInfo'
import {
    ResourceType,
    SimpleCallback,
    Return2Callback,
    ReturnCallback,
    SubTree,
    OpenWriteStreamMode,
    ResourcePropertyValue,
    PropertyAttributes
} from './CommonTypes'
import { ISerializableFileSystem, FileSystemSerializer } from './Serialization'
import { BasicPrivilege, PrivilegeManager } from '../../../user/v2/privilege/PrivilegeManager'
import { FileSystemEvent, WebDAVServer } from '../../../server/v2/webDAVServer/WebDAVServer'
import { Readable, Writable, Transform } from 'stream'
import { IPropertyManager, PropertyBag } from './PropertyManager'
import { ContextualFileSystem } from './ContextualFileSystem'
import { StandardMethods } from './StandardMethods'
import { RequestContext } from '../../../server/v2/RequestContext'
import { ILockManager, ILockManagerAsync } from './LockManager'
import { LockScope } from '../../../resource/v2/lock/LockScope'
import { LockType } from '../../../resource/v2/lock/LockType'
import { LockKind } from '../../../resource/v2/lock/LockKind'
import { Workflow } from '../../../helper/Workflow'
import { Resource } from './Resource'
import { Errors } from '../../../Errors'
import { Lock } from '../../../resource/v2/lock/Lock'
import { Path } from '../Path'
import * as crypto from 'crypto'
import { ensureValue, promisifyCall } from '../../../helper/v2/promise'

class BufferedIsLocked
{
    _isLocked : boolean;

    constructor(public fs : FileSystem, public ctx : RequestContext, public path : Path)
    {
        this._isLocked = null;
    }

    isLocked(callback : ReturnCallback<boolean>)
    {
        if(this._isLocked !== null)
            return callback(null, this._isLocked);
        
        this.fs.isLocked(this.ctx, this.path, (e, locked) => {
            if(e)
                return callback(e);
            
            this._isLocked = locked;
            callback(null, locked);
        })
    }
}

/**
 * File system which manage resources under its mounted path.
 * 
 * @see https://github.com/OpenMarshal/npm-WebDAV-Server/wiki/Custom-File-System-%5Bv2%5D
 */
export abstract class FileSystem implements ISerializableFileSystem
{
    private __serializer;

    constructor(serializer : FileSystemSerializer)
    {
        this.__serializer = serializer;
    }
    
    /**
     * Get the serializer.
     */
    serializer() : FileSystemSerializer
    {
        return this.__serializer;
    }

    /**
     * Defines the serializer to use.
     * 
     * @param serializer Serializer to use.
     */
    setSerializer(serializer : FileSystemSerializer)
    {
        this.__serializer = serializer;
    }

    /**
     * Tell to not serialize this file system.
     */
    doNotSerialize()
    {
        this.__serializer = null;
    }

    /**
     * Wrap the file system with the context.
     * 
     * @param ctx Context of the operation.
     */
    contextualize(ctx : RequestContext) : ContextualFileSystem
    {
        return new ContextualFileSystem(this, ctx);
    }

    /**
     * Wrap the file system with the context and a resource path.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     */
    resource(ctx : RequestContext, path : Path) : Resource
    {
        return new Resource(path, this, ctx);
    }
    
    /**
     * Make a fast check if the resource exists.
     * If '_fastExistCheck' is not implemented, this method call 'callback'.
     * If '_fastExistCheck' is implemented and it returns 'false', then the 'errorCallback' is called, otherwise the 'callback' is called.
     * 
     * This method will not give a true information, but just an estimate of the existence of a resource.
     * 
     * @param ctx Context of the operation.
     * @param _path Path of the resource.
     * @param errorCallback Callback to call when the resource is sure to not exist.
     * @param callback Callback to call when the resource might exists.
     */
    fastExistCheckEx(ctx : RequestContext, _path : Path | string, errorCallback : SimpleCallback, callback : () => void) : void
    {
        if(!this._fastExistCheck)
            return callback();
        
        const path = new Path(_path);
        this._fastExistCheck(ctx, path, (exists) => {
            if(!exists)
                errorCallback(Errors.ResourceNotFound);
            else
                callback();
        });
    }
    
    /**
     * Make a fast check if the resource exists.
     * If '_fastExistCheck' is not implemented, this method call 'callback'.
     * If '_fastExistCheck' is implemented and it returns 'false', then the 'callback' is called, otherwise the 'errorCallback' is called.
     * 
     * This method will not give a true information, but just an estimate of the existence of a resource.
     * 
     * @param ctx Context of the operation.
     * @param _path Path of the resource.
     * @param errorCallback Callback to call when the resource might exists.
     * @param callback Callback to call when the resource is sure to not exist.
     */
    fastExistCheckExReverse(ctx : RequestContext, _path : Path | string, errorCallback : SimpleCallback, callback : () => void) : void
    {
        if(!this._fastExistCheck)
            return callback();
        
        const path = new Path(_path);
        this._fastExistCheck(ctx, path, (exists) => {
            if(exists)
                errorCallback(Errors.ResourceAlreadyExists);
            else
                callback();
        });
    }

    /**
     * Make a fast check if a resource exists.
     * This method will call '_fastExistCheck' if it is implemented or return 'true'.
     * 
     * This method will not give a true information, but just an estimate of the existence of a resource.
     * 
     * @param ctx Context of the operation.
     * @param _path Path of the resource.
     * @param callback Returns if the resource exists.
     */
    protected fastExistCheck(ctx : RequestContext, _path : Path | string, callback : (exists : boolean) => void) : void
    {
        if(!this._fastExistCheck)
            return callback(true);
        
        const path = new Path(_path);
        this._fastExistCheck(ctx, path, (exists) => callback(!!exists));
    }
    protected _fastExistCheck?(ctx : RequestContext, path : Path, callback : (exists : boolean) => void) : void

    /**
     * Create a new resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param type Type of the resource to create.
     */
    createAsync(ctx : RequestContext, path : Path | string, type : ResourceType) : Promise<void>
    /**
     * Create a new resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param type Type of the resource to create.
     * @param createIntermediates Defines if the operation is allowed to create intermediate resources ('/folder1/folder2/file3', if 'folder2' doesn't exist, it is an intermediate).
     */
    createAsync(ctx : RequestContext, path : Path | string, type : ResourceType, createIntermediates : boolean) : Promise<void>
    createAsync(ctx : RequestContext, path : Path | string, type : ResourceType, createIntermediates ?: boolean) : Promise<void>
    {
        return promisifyCall((cb) => this.create(ctx, path, type, createIntermediates, cb));
    }

    /**
     * Create a new resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param type Type of the resource to create.
     * @param callback Returns an error if one occured.
     */
    create(ctx : RequestContext, path : Path | string, type : ResourceType, callback : SimpleCallback) : void
    /**
     * Create a new resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param type Type of the resource to create.
     * @param createIntermediates Defines if the operation is allowed to create intermediate resources ('/folder1/folder2/file3', if 'folder2' doesn't exist, it is an intermediate).
     * @param callback Returns an error if one occured.
     */
    create(ctx : RequestContext, path : Path | string, type : ResourceType, createIntermediates : boolean, callback : SimpleCallback) : void
    create(ctx : RequestContext, _path : Path | string, type : ResourceType, _createIntermediates : boolean | SimpleCallback, _callback ?: SimpleCallback) : void
    {
        const createIntermediates = ensureValue(_callback ? _createIntermediates as boolean : undefined, false);
        const callbackFinal = _callback ? _callback : _createIntermediates as SimpleCallback;
        const path = new Path(_path);

        const callback : SimpleCallback = (e) => {
            if(!e)
                this.emit('create', ctx, path, { type, createIntermediates })
            callbackFinal(e);
        }

        if(!this._create)
            return callback(Errors.InvalidOperation);

        this.emit('before-create', ctx, path, { type, createIntermediates })
        
        issuePrivilegeCheck(this, ctx, path, 'canWrite', callback, () => {
            const go = () => {
                ctx.server.options.storageManager.evaluateCreate(ctx, this, path, type, (size) => {
                ctx.server.options.storageManager.reserve(ctx, this, size, (reserved) => {
                    if(!reserved)
                        return callback(Errors.InsufficientStorage);

                    this._create(path, {
                        context: ctx,
                        type
                    }, (e) => {
                        if(e)
                            ctx.server.options.storageManager.reserve(ctx, this, -size, () => callback(e));
                        else
                            callback();
                    });
                })
                })
            }

            this.isLocked(ctx, path, (e, locked) => {
                if(e || locked)
                    return callback(locked ? Errors.Locked : e);
                
                this.fastExistCheckExReverse(ctx, path, callback, () => {
                    this.type(ctx, path.getParent(), (e, type) => {
                        if(e === Errors.ResourceNotFound)
                        {
                            if(!createIntermediates)
                                return callback(Errors.IntermediateResourceMissing);

                            this.getFullPath(ctx, path, (e, fullPath) => {
                                if(e)
                                    return callback(e);
                                
                                fullPath = fullPath.getParent();
                                ctx.getResource(fullPath, (e, r) => {
                                    if(e)
                                        return callback(e);
                                    
                                    r.create(ResourceType.Directory, true, (e) => {
                                        if(e && e !== Errors.ResourceAlreadyExists)
                                            return callback(e);
                                        
                                        go();
                                    })
                                })
                            })
                            return;
                        }
                        if(e)
                            return callback(e);
                        
                        if(!type.isDirectory)
                            return callback(Errors.WrongParentTypeForCreation);

                        go();
                    })
                })
            })
        })
    }
    protected _create?(path : Path, ctx : CreateInfo, callback : SimpleCallback) : void

    /**
     * Get the etag of the resource.
     * The default etag, if '_etag' is not implemented, is to hash the last modified date information of the resource and wrap it with quotes.
     * 
     * @param ctx Context of the operation.
     * @param _path Path of the resource.
     */
    etagAsync(ctx : RequestContext, path : Path | string) : Promise<string>
    {
        return promisifyCall((cb) => this.etag(ctx, path, cb))
    }

    /**
     * Get the etag of the resource.
     * The default etag, if '_etag' is not implemented, is to hash the last modified date information of the resource and wrap it with quotes.
     * 
     * @param ctx Context of the operation.
     * @param _path Path of the resource.
     * @param callback Returns the etag of the resource.
     */
    etag(ctx : RequestContext, _path : Path | string, callback : ReturnCallback<string>) : void
    {
        const path = new Path(_path);

        issuePrivilegeCheck(this, ctx, path, 'canReadProperties', callback, () => {
            this.fastExistCheckEx(ctx, path, callback, () => {
                if(!this._etag)
                    return this.lastModifiedDate(ctx, path, (e, date) => {
                        if(e)
                            return callback(e);
                        
                        date = FileSystem.neutralizeEmptyDate(date);

                        callback(null, '"' + crypto.createHash('md5').update(date.toString()).digest('hex') + '"');
                    })

                this._etag(path, {
                    context: ctx
                }, callback);
            })
        })
    }
    protected _etag?(path : Path, ctx : ETagInfo, callback : ReturnCallback<string>) : void

    /**
     * Delete a resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     */
    deleteAsync(ctx : RequestContext, path : Path | string) : Promise<void>
    /**
     * Delete a resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param depth Depth of the delete. Might be ignored depending on the implementation.
     */
    deleteAsync(ctx : RequestContext, path : Path | string, depth : number) : Promise<void>
    deleteAsync(ctx : RequestContext, path : Path | string, depth ?: number) : Promise<void>
    {
        return promisifyCall((cb) => this.delete(ctx, path, depth, cb))
    }

    /**
     * Delete a resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param callback Returns an error if one occured.
     */
    delete(ctx : RequestContext, path : Path | string, callback : SimpleCallback) : void
    /**
     * Delete a resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param depth Depth of the delete. Might be ignored depending on the implementation.
     * @param callback Returns an error if one occured.
     */
    delete(ctx : RequestContext, path : Path | string, depth : number, callback : SimpleCallback) : void
    delete(ctx : RequestContext, _path : Path | string, _depth : number | SimpleCallback, _callback ?: SimpleCallback) : void
    {
        const depth = ensureValue(_callback ? _depth as number : undefined, -1);
        const callbackFinal = _callback ? _callback : _depth as SimpleCallback;
        const path = new Path(_path);

        const callback : SimpleCallback = (e) => {
            if(!e)
                this.emit('delete', ctx, path, { depth })
            callbackFinal(e);
        }

        if(!this._delete)
            return callback(Errors.InvalidOperation);

        this.emit('before-delete', ctx, path, { depth })

        issuePrivilegeCheck(this, ctx, path, 'canWrite', callback, () => {
            this.isLocked(ctx, path, (e, isLocked) => {
                if(e || isLocked)
                    return callback(e ? e : Errors.Locked);
                
                this.fastExistCheckEx(ctx, path, callback, () => {
                    this.size(ctx, path, (e, contentSize) => {
                        contentSize = contentSize || 0;

                        this._delete(path, {
                            context: ctx,
                            depth
                        }, (e) => {
                            if(!e)
                            {
                                this.type(ctx, path, (e, type) => {
                                    ctx.server.options.storageManager.evaluateContent(ctx, this, contentSize, (reservedContentSize) => {
                                    ctx.server.options.storageManager.evaluateCreate(ctx, this, path, type, (size) => {
                                        ctx.server.options.storageManager.reserve(ctx, this, -size - reservedContentSize, () => {
                                            callback();
                                        })
                                    })
                                    })
                                })
                            }
                            else
                                callback(e);
                        });
                    });
                })
            })
        })
    }
    protected _delete?(path : Path, ctx : DeleteInfo, callback : SimpleCallback) : void
    
    /**
     * Open a stream to write the content of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     */
    openWriteStreamAsync(ctx : RequestContext, path : Path | string) : Promise<{ stream : Writable, created : boolean }>
    /**
     * Open a stream to write the content of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param estimatedSize Estimate of the size to write.
     */
    openWriteStreamAsync(ctx : RequestContext, path : Path | string, estimatedSize : number) : Promise<{ stream : Writable, created : boolean }>
    /**
     * Open a stream to write the content of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param targetSource Define if the content must be the source or the computed content. Might make no difference depending on the implementation.
     */
    openWriteStreamAsync(ctx : RequestContext, path : Path | string, targetSource : boolean) : Promise<{ stream : Writable, created : boolean }>
    /**
     * Open a stream to write the content of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param targetSource Define if the content must be the source or the computed content. Might make no difference depending on the implementation.
     * @param estimatedSize Estimate of the size to write.
     */
    openWriteStreamAsync(ctx : RequestContext, path : Path | string, targetSource : boolean, estimatedSize : number) : Promise<{ stream : Writable, created : boolean }>
    /**
     * Open a stream to write the content of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param mode Define if this operation can/must create a new resource and/or its intermediate resources ('/folder1/folder2/file3', if 'folder2' doesn't exist, it is an intermediate).
     */
    openWriteStreamAsync(ctx : RequestContext, path : Path | string, mode : OpenWriteStreamMode) : Promise<{ stream : Writable, created : boolean }>
    /**
     * Open a stream to write the content of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param mode Define if this operation can/must create a new resource and/or its intermediate resources ('/folder1/folder2/file3', if 'folder2' doesn't exist, it is an intermediate).
     * @param estimatedSize Estimate of the size to write.
     */
    openWriteStreamAsync(ctx : RequestContext, path : Path | string, mode : OpenWriteStreamMode, estimatedSize : number) : Promise<{ stream : Writable, created : boolean }>
    /**
     * Open a stream to write the content of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param mode Define if this operation can/must create a new resource and/or its intermediate resources ('/folder1/folder2/file3', if 'folder2' doesn't exist, it is an intermediate).
     * @param targetSource Define if the content must be the source or the computed content. Might make no difference depending on the implementation.
     */
    openWriteStreamAsync(ctx : RequestContext, path : Path | string, mode : OpenWriteStreamMode, targetSource : boolean) : Promise<{ stream : Writable, created : boolean }>
    /**
     * Open a stream to write the content of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param mode Define if this operation can/must create a new resource and/or its intermediate resources ('/folder1/folder2/file3', if 'folder2' doesn't exist, it is an intermediate).
     * @param targetSource Define if the content must be the source or the computed content. Might make no difference depending on the implementation.
     * @param estimatedSize Estimate of the size to write.
     */
    openWriteStreamAsync(ctx : RequestContext, path : Path | string, mode : OpenWriteStreamMode, targetSource : boolean, estimatedSize : number) : Promise<{ stream : Writable, created : boolean }>
    openWriteStreamAsync(ctx : RequestContext, path : Path | string, mode ?: any, targetSource ?: any, estimatedSize ?: any) : Promise<{ stream : Writable, created : boolean }>
    {
        return promisifyCall((cb) => this.openWriteStream(ctx, path, mode, targetSource, estimatedSize, (e, data1, data2) => cb(e, e ? undefined : { stream: data1, created: data2 })));
    }

    /**
     * Open a stream to write the content of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param callback Returns the stream.
     */
    openWriteStream(ctx : RequestContext, path : Path | string, callback : Return2Callback<Writable, boolean>) : void
    /**
     * Open a stream to write the content of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param estimatedSize Estimate of the size to write.
     * @param callback Returns the stream.
     */
    openWriteStream(ctx : RequestContext, path : Path | string, estimatedSize : number, callback : Return2Callback<Writable, boolean>) : void
    /**
     * Open a stream to write the content of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param targetSource Define if the content must be the source or the computed content. Might make no difference depending on the implementation.
     * @param callback Returns the stream.
     */
    openWriteStream(ctx : RequestContext, path : Path | string, targetSource : boolean, callback : Return2Callback<Writable, boolean>) : void
    /**
     * Open a stream to write the content of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param targetSource Define if the content must be the source or the computed content. Might make no difference depending on the implementation.
     * @param estimatedSize Estimate of the size to write.
     * @param callback Returns the stream.
     */
    openWriteStream(ctx : RequestContext, path : Path | string, targetSource : boolean, estimatedSize : number, callback : Return2Callback<Writable, boolean>) : void
    /**
     * Open a stream to write the content of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param mode Define if this operation can/must create a new resource and/or its intermediate resources ('/folder1/folder2/file3', if 'folder2' doesn't exist, it is an intermediate).
     * @param callback Returns the stream.
     */
    openWriteStream(ctx : RequestContext, path : Path | string, mode : OpenWriteStreamMode, callback : Return2Callback<Writable, boolean>) : void
    /**
     * Open a stream to write the content of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param mode Define if this operation can/must create a new resource and/or its intermediate resources ('/folder1/folder2/file3', if 'folder2' doesn't exist, it is an intermediate).
     * @param estimatedSize Estimate of the size to write.
     * @param callback Returns the stream.
     */
    openWriteStream(ctx : RequestContext, path : Path | string, mode : OpenWriteStreamMode, estimatedSize : number, callback : Return2Callback<Writable, boolean>) : void
    /**
     * Open a stream to write the content of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param mode Define if this operation can/must create a new resource and/or its intermediate resources ('/folder1/folder2/file3', if 'folder2' doesn't exist, it is an intermediate).
     * @param targetSource Define if the content must be the source or the computed content. Might make no difference depending on the implementation.
     * @param callback Returns the stream.
     */
    openWriteStream(ctx : RequestContext, path : Path | string, mode : OpenWriteStreamMode, targetSource : boolean, callback : Return2Callback<Writable, boolean>) : void
    /**
     * Open a stream to write the content of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param mode Define if this operation can/must create a new resource and/or its intermediate resources ('/folder1/folder2/file3', if 'folder2' doesn't exist, it is an intermediate).
     * @param targetSource Define if the content must be the source or the computed content. Might make no difference depending on the implementation.
     * @param estimatedSize Estimate of the size to write.
     * @param callback Returns the stream.
     */
    openWriteStream(ctx : RequestContext, path : Path | string, mode : OpenWriteStreamMode, targetSource : boolean, estimatedSize : number, callback : Return2Callback<Writable, boolean>) : void
    openWriteStream(ctx : RequestContext, _path : Path | string, _mode : OpenWriteStreamMode | boolean | number | Return2Callback<Writable, boolean>, _targetSource ?: boolean | number | Return2Callback<Writable, boolean>, _estimatedSize ?: number | Return2Callback<Writable, boolean>, _callback ?: Return2Callback<Writable, boolean>) : void
    {
        let targetSource = false;
        for(const obj of [ _mode, _targetSource ])
            if(obj && obj.constructor === Boolean)
                targetSource = obj as boolean;

        let estimatedSize = -1;
        for(const obj of [ _mode, _targetSource, _estimatedSize ])
            if(obj && obj.constructor === Number)
                estimatedSize = obj as number;

        let callbackFinal;
        for(const obj of [ _mode, _targetSource, _estimatedSize, _callback ])
            if(obj && obj.constructor === Function)
                callbackFinal = obj as Return2Callback<Writable, boolean>;
        
        const mode = _mode && _mode.constructor === String ? _mode as OpenWriteStreamMode : 'mustExist';
        const path = new Path(_path);
        let created = false;

        const callback : Return2Callback<Writable, boolean> = (e, stream, created) => {
            if(!e)
                this.emit('openWriteStream', ctx, path, { targetSource, mode, estimatedSize, created, stream })
            callbackFinal(e, stream, created);
        }
        
        if(!this._openWriteStream)
            return callback(Errors.InvalidOperation);

        this.emit('before-openWriteStream', ctx, path, { targetSource, mode, estimatedSize, created })
        
        issuePrivilegeCheck(this, ctx, path, targetSource ? 'canWriteContentSource' : 'canWriteContentTranslated', callback, () => {
            this.isLocked(ctx, path, (e, isLocked) => {
                if(e || isLocked)
                    return callback(e ? e : Errors.Locked);
                
                const finalGo = (callback : Return2Callback<Writable, boolean>) =>
                {
                    this._openWriteStream(path, {
                        context: ctx,
                        estimatedSize,
                        targetSource,
                        mode
                    }, (e, wStream) => callback(e, wStream, created));
                }
                const go = (callback : Return2Callback<Writable, boolean>) =>
                {
                    this.size(ctx, path, true, (e, size) => {
                        ctx.server.options.storageManager.evaluateContent(ctx, this, size, (sizeStored) => {
                            if(estimatedSize === undefined || estimatedSize === null || estimatedSize.constructor === Number && estimatedSize <= 0)
                            {
                                ctx.server.options.storageManager.available(ctx, this, (available) => {
                                    if(available === -1)
                                        return finalGo(callback);
                                    if(available === 0)
                                        return callback(Errors.InsufficientStorage);

                                    let nb = 0;
                                    finalGo((e, wStream, created) => {
                                        if(e)
                                            return callback(e, wStream, created);

                                        const stream = new Transform({
                                            transform(chunk, encoding, callback)
                                            {
                                                nb += chunk.length;
                                                if(nb > available)
                                                    callback(Errors.InsufficientStorage);
                                                else
                                                    callback(null, chunk, encoding);
                                            }
                                        });
                                        stream.pipe(wStream);
                                        stream.on('finish', () => {
                                            ctx.server.options.storageManager.reserve(ctx, this, nb, (reserved) => {
                                                if(!reserved)
                                                    stream.emit('error', Errors.InsufficientStorage);
                                            })
                                        })
                                        callback(e, stream, created);
                                    })
                                })
                            }
                            else
                            {
                                ctx.server.options.storageManager.evaluateContent(ctx, this, estimatedSize, (estimatedSizeStored) => {
                                ctx.server.options.storageManager.reserve(ctx, this, estimatedSizeStored - sizeStored, (reserved) => {
                                    if(!reserved)
                                        return callback(Errors.InsufficientStorage);
                                    finalGo(callback);
                                })
                                })
                            }
                        })
                    })
                }

                const createAndGo = (intermediates : boolean) =>
                {
                    this.create(ctx, path, ResourceType.File, intermediates, (e) => {
                        if(e)
                            return callback(e);
                        
                        created = true;
                        go(callback);
                    })
                }

                switch(mode)
                {
                    case 'mustExist':
                        this.fastExistCheckEx(ctx, path, callback, () => go(callback));
                        break;
                    
                    case 'mustCreateIntermediates':
                    case 'mustCreate':
                        createAndGo(mode === 'mustCreateIntermediates');
                        break;
                    
                    case 'canCreateIntermediates':
                    case 'canCreate':
                        go((e, wStream) => {
                            if(e === Errors.ResourceNotFound)
                                createAndGo(mode === 'canCreateIntermediates');
                            else
                                callback(e, wStream);
                        })
                        break;
                    
                    default:
                        callback(Errors.IllegalArguments);
                        break;
                }
            })
        })
    }
    protected _openWriteStream?(path : Path, ctx : OpenWriteStreamInfo, callback : ReturnCallback<Writable>) : void
    
    /**
     * Open a stream to read the content of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     */
    openReadStreamAsync(ctx : RequestContext, path : Path | string) : Promise<Readable>
    /**
     * Open a stream to read the content of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param estimatedSize Estimate of the size to read.
     */
    openReadStreamAsync(ctx : RequestContext, path : Path | string, estimatedSize : number) : Promise<Readable>
    /**
     * Open a stream to read the content of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param targetSource Define if the content must be the source or the computed content. Might make no difference depending on the implementation.
     */
    openReadStreamAsync(ctx : RequestContext, path : Path | string, targetSource : boolean) : Promise<Readable>
    /**
     * Open a stream to read the content of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param targetSource Define if the content must be the source or the computed content. Might make no difference depending on the implementation.
     * @param estimatedSize Estimate of the size to read.
     */
    openReadStreamAsync(ctx : RequestContext, path : Path | string, targetSource : boolean, estimatedSize : number) : Promise<Readable>
    openReadStreamAsync(ctx : RequestContext, path : Path | string, targetSource ?: any, estimatedSize ?: any) : Promise<Readable>
    {
        return promisifyCall((cb) => this.openReadStream(ctx, path, targetSource, estimatedSize, cb))
    }
    
    /**
     * Open a stream to read the content of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param callback Returns the stream.
     */
    openReadStream(ctx : RequestContext, path : Path | string, callback : ReturnCallback<Readable>) : void
    /**
     * Open a stream to read the content of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param estimatedSize Estimate of the size to read.
     * @param callback Returns the stream.
     */
    openReadStream(ctx : RequestContext, path : Path | string, estimatedSize : number, callback : ReturnCallback<Readable>) : void
    /**
     * Open a stream to read the content of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param targetSource Define if the content must be the source or the computed content. Might make no difference depending on the implementation.
     * @param callback Returns the stream.
     */
    openReadStream(ctx : RequestContext, path : Path | string, targetSource : boolean, callback : ReturnCallback<Readable>) : void
    /**
     * Open a stream to read the content of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param targetSource Define if the content must be the source or the computed content. Might make no difference depending on the implementation.
     * @param estimatedSize Estimate of the size to read.
     * @param callback Returns the stream.
     */
    openReadStream(ctx : RequestContext, path : Path | string, targetSource : boolean, estimatedSize : number, callback : ReturnCallback<Readable>) : void
    openReadStream(ctx : RequestContext, _path : Path | string, _targetSource : boolean | number | ReturnCallback<Readable>, _estimatedSize ?: number | ReturnCallback<Readable>, _callback ?: ReturnCallback<Readable>) : void
    {
        const targetSource = ensureValue(_targetSource.constructor === Boolean ? _targetSource as boolean : undefined, false);
        const estimatedSize = ensureValue(_callback ? _estimatedSize as number : _estimatedSize ? _targetSource as number : undefined, -1);
        const callbackFinal = _callback ? _callback : _estimatedSize ? _estimatedSize as ReturnCallback<Readable> : _targetSource as ReturnCallback<Readable>;
        const path = new Path(_path);

        const callback : ReturnCallback<Readable> = (e, stream) => {
            if(!e)
                this.emit('openReadStream', ctx, path, { targetSource, estimatedSize, stream })
            callbackFinal(e, stream);
        }

        this.emit('before-openReadStream', ctx, path, { targetSource, estimatedSize })
        
        issuePrivilegeCheck(this, ctx, path, targetSource ? 'canReadContentSource' : 'canReadContentTranslated', callback, () => {
            this.fastExistCheckEx(ctx, path, callback, () => {
                if(!this._openReadStream)
                    return callback(Errors.InvalidOperation);

                this._openReadStream(path, {
                    context: ctx,
                    estimatedSize,
                    targetSource
                }, callback);
            })
        })
    }
    protected _openReadStream?(path : Path, ctx : OpenReadStreamInfo, callback : ReturnCallback<Readable>) : void

    /**
     * Move a resource.
     * 
     * @param ctx Context of the operation.
     * @param pathFrom Path of the resource to move.
     * @param pathTo Destination path to where move the resource.
     */
    moveAsync(ctx : RequestContext, pathFrom : Path | string, pathTo : Path | string) : Promise<boolean>
    /**
     * Move a resource.
     * 
     * @param ctx Context of the operation.
     * @param pathFrom Path of the resource to move.
     * @param pathTo Destination path to where move the resource.
     * @param overwrite 
     */
    moveAsync(ctx : RequestContext, pathFrom : Path | string, pathTo : Path | string, overwrite : boolean) : Promise<boolean>
    moveAsync(ctx : RequestContext, pathFrom : Path | string, pathTo : Path | string, overwrite ?: boolean) : Promise<boolean>
    {
        return promisifyCall((cb) => this.move(ctx, pathFrom, pathTo, overwrite, cb))
    }

    /**
     * Move a resource.
     * 
     * @param ctx Context of the operation.
     * @param pathFrom Path of the resource to move.
     * @param pathTo Destination path to where move the resource.
     * @param callback Returns if the resource has been owerwritten.
     */
    move(ctx : RequestContext, pathFrom : Path | string, pathTo : Path | string, callback : ReturnCallback<boolean>) : void
    /**
     * Move a resource.
     * 
     * @param ctx Context of the operation.
     * @param pathFrom Path of the resource to move.
     * @param pathTo Destination path to where move the resource.
     * @param overwrite 
     * @param callback Returns if the resource has been owerwritten.
     */
    move(ctx : RequestContext, pathFrom : Path | string, pathTo : Path | string, overwrite : boolean, callback : ReturnCallback<boolean>) : void
    move(ctx : RequestContext, _pathFrom : Path | string, _pathTo : Path | string, _overwrite : boolean | ReturnCallback<boolean>, _callback ?: ReturnCallback<boolean>) : void
    {
        const callbackFinal = _callback ? _callback : _overwrite as ReturnCallback<boolean>;
        const overwrite = ensureValue(_callback ? _overwrite as boolean : undefined, false);
        const pathFrom = new Path(_pathFrom);
        const pathTo = new Path(_pathTo);

        const callback : ReturnCallback<boolean> = (e, overrided) => {
            if(!e)
                this.emit('move', ctx, pathFrom, { pathFrom, pathTo, overwrite, overrided })
            callbackFinal(e, overrided);
        }

        this.emit('before-move', ctx, pathFrom, { pathFrom, pathTo, overwrite })

        issuePrivilegeCheck(this, ctx, pathFrom, 'canRead', callback, () => {
        issuePrivilegeCheck(this, ctx, pathTo, 'canWrite', callback, () => {
            this.isLocked(ctx, pathFrom, (e, isLocked) => {
                if(e || isLocked)
                    return callback(e ? e : Errors.Locked);
                
                    this.isLocked(ctx, pathTo, (e, isLocked) => {
                        if(e || isLocked)
                            return callback(e ? e : Errors.Locked);
                        
                    const go = () =>
                    {
                        if(this._move)
                        {
                            this._move(pathFrom, pathTo, {
                                context: ctx,
                                overwrite
                            }, callback);
                            return;
                        }

                        StandardMethods.standardMove(ctx, pathFrom, this, pathTo, this, overwrite, callback);
                    }

                    this.fastExistCheckEx(ctx, pathFrom, callback, () => {
                        if(!overwrite)
                            this.fastExistCheckExReverse(ctx, pathTo, callback, go);
                        else
                            go();
                    })
                })
            })
        })
        })
    }
    protected _move?(pathFrom : Path, pathTo : Path, ctx : MoveInfo, callback : ReturnCallback<boolean>) : void

    /**
     * Copy a resource.
     * 
     * @param ctx Context of the operation.
     * @param pathFrom Path of the resource to copy.
     * @param pathTo Destination path to where copy the resource.
     */
    copyAsync(ctx : RequestContext, pathFrom : Path | string, pathTo : Path | string) : Promise<boolean>
    /**
     * Copy a resource.
     * 
     * @param ctx Context of the operation.
     * @param pathFrom Path of the resource to copy.
     * @param pathTo Destination path to where copy the resource.
     * @param depth Depth to make the copy. (Infinite = -1)
     */
    copyAsync(ctx : RequestContext, pathFrom : Path | string, pathTo : Path | string, depth : number) : Promise<boolean>
    /**
     * Copy a resource.
     * 
     * @param ctx Context of the operation.
     * @param pathFrom Path of the resource to copy.
     * @param pathTo Destination path to where copy the resource.
     * @param overwrite 
     */
    copyAsync(ctx : RequestContext, pathFrom : Path | string, pathTo : Path | string, overwrite : boolean) : Promise<boolean>
    /**
     * Copy a resource.
     * 
     * @param ctx Context of the operation.
     * @param pathFrom Path of the resource to copy.
     * @param pathTo Destination path to where copy the resource.
     * @param overwrite 
     * @param depth Depth to make the copy. (Infinite = -1)
     */
    copyAsync(ctx : RequestContext, pathFrom : Path | string, pathTo : Path | string, overwrite : boolean, depth : number) : Promise<boolean>
    copyAsync(ctx : RequestContext, pathFrom : Path | string, pathTo : Path | string, overwrite ?: any, depth ?: any) : Promise<boolean>
    {
        return promisifyCall((cb) => this.copy(ctx, pathFrom, pathTo, overwrite, depth))
    }

    /**
     * Copy a resource.
     * 
     * @param ctx Context of the operation.
     * @param pathFrom Path of the resource to copy.
     * @param pathTo Destination path to where copy the resource.
     * @param callback Returns if the resource has been owerwritten.
     */
    copy(ctx : RequestContext, pathFrom : Path | string, pathTo : Path | string, callback : ReturnCallback<boolean>) : void
    /**
     * Copy a resource.
     * 
     * @param ctx Context of the operation.
     * @param pathFrom Path of the resource to copy.
     * @param pathTo Destination path to where copy the resource.
     * @param depth Depth to make the copy. (Infinite = -1)
     * @param callback Returns if the resource has been owerwritten.
     */
    copy(ctx : RequestContext, pathFrom : Path | string, pathTo : Path | string, depth : number, callback : ReturnCallback<boolean>) : void
    /**
     * Copy a resource.
     * 
     * @param ctx Context of the operation.
     * @param pathFrom Path of the resource to copy.
     * @param pathTo Destination path to where copy the resource.
     * @param overwrite 
     * @param callback Returns if the resource has been owerwritten.
     */
    copy(ctx : RequestContext, pathFrom : Path | string, pathTo : Path | string, overwrite : boolean, callback : ReturnCallback<boolean>) : void
    /**
     * Copy a resource.
     * 
     * @param ctx Context of the operation.
     * @param pathFrom Path of the resource to copy.
     * @param pathTo Destination path to where copy the resource.
     * @param overwrite 
     * @param depth Depth to make the copy. (Infinite = -1)
     * @param callback Returns if the resource has been owerwritten.
     */
    copy(ctx : RequestContext, pathFrom : Path | string, pathTo : Path | string, overwrite : boolean, depth : number, callback : ReturnCallback<boolean>) : void
    copy(ctx : RequestContext, _pathFrom : Path | string, _pathTo : Path | string, _overwrite : boolean | number | ReturnCallback<boolean>, _depth ?: number | ReturnCallback<boolean>, _callback ?: ReturnCallback<boolean>) : void
    {
        const overwrite = ensureValue(_overwrite.constructor === Boolean ? _overwrite as boolean : undefined, false);
        const depth = ensureValue(_callback ? _depth as number : !_depth ? -1 : _overwrite.constructor === Number ? _overwrite as number : undefined, -1);
        const callbackFinal = _callback ? _callback : _depth ? _depth as ReturnCallback<boolean> : _overwrite as ReturnCallback<boolean>;
        const pathFrom = new Path(_pathFrom);
        const pathTo = new Path(_pathTo);

        const callback : ReturnCallback<boolean> = (e, overrided) => {
            if(!e)
                this.emit('copy', ctx, pathFrom, { pathTo, overwrite, overrided, depth })
            callbackFinal(e, overrided);
        }
        
        this.emit('before-copy', ctx, pathFrom, { pathTo, overwrite, depth })

        issuePrivilegeCheck(this, ctx, pathFrom, 'canRead', callback, () => {
        issuePrivilegeCheck(this, ctx, pathTo, 'canWrite', callback, () => {
            this.isLocked(ctx, pathTo, (e, isLocked) => {
                if(e || isLocked)
                    return callback(e ? e : Errors.Locked);
                
                const go = () =>
                {
                    if(this._copy)
                    {
                        this._copy(pathFrom, pathTo, {
                            context: ctx,
                            depth,
                            overwrite
                        }, callback);
                        return;
                    }
                    
                    StandardMethods.standardCopy(ctx, pathFrom, this, pathTo, this, overwrite, depth, callback);
                }
                
                this.fastExistCheckEx(ctx, pathFrom, callback, () => {
                    if(!overwrite)
                        this.fastExistCheckExReverse(ctx, pathTo, callback, go);
                    else
                        go();
                })
            })
        })
        })
    }
    protected _copy?(pathFrom : Path, pathTo : Path, ctx : CopyInfo, callback : ReturnCallback<boolean>) : void

    /**
     * Rename the resource.
     * By default, if the '_rename' method is not implemented, it makes a move.
     * 
     * @param ctx Context of the operation.
     * @param pathFrom Path of the resource to rename.
     * @param newName New name of the resource.
     */
    renameAsync(ctx : RequestContext, pathFrom : Path | string, newName : string) : Promise<boolean>
    /**
     * Rename the resource.
     * By default, if the '_rename' method is not implemented, it makes a move.
     * 
     * @param ctx Context of the operation.
     * @param pathFrom Path of the resource to rename.
     * @param newName New name of the resource.
     * @param overwrite 
     */
    renameAsync(ctx : RequestContext, pathFrom : Path | string, newName : string, overwrite : boolean) : Promise<boolean>
    renameAsync(ctx : RequestContext, pathFrom : Path | string, newName : string, overwrite ?: boolean) : Promise<boolean>
    {
        return promisifyCall((cb) => this.rename(ctx, pathFrom, newName, overwrite, cb))
    }

    /**
     * Rename the resource.
     * By default, if the '_rename' method is not implemented, it makes a move.
     * 
     * @param ctx Context of the operation.
     * @param pathFrom Path of the resource to rename.
     * @param newName New name of the resource.
     * @param callback Returns if the resource has been owerwritten.
     */
    rename(ctx : RequestContext, pathFrom : Path | string, newName : string, callback : ReturnCallback<boolean>) : void
    /**
     * Rename the resource.
     * By default, if the '_rename' method is not implemented, it makes a move.
     * 
     * @param ctx Context of the operation.
     * @param pathFrom Path of the resource to rename.
     * @param newName New name of the resource.
     * @param overwrite 
     * @param callback Returns if the resource has been owerwritten.
     */
    rename(ctx : RequestContext, pathFrom : Path | string, newName : string, overwrite : boolean, callback : ReturnCallback<boolean>) : void
    rename(ctx : RequestContext, _pathFrom : Path | string, newName : string, _overwrite : boolean | ReturnCallback<boolean>, _callback ?: ReturnCallback<boolean>) : void
    {
        const overwrite = ensureValue(_callback ? _overwrite as boolean : undefined, false);
        const callbackFinal = _callback ? _callback : _overwrite as ReturnCallback<boolean>;
        const pathFrom = new Path(_pathFrom);

        const callback : ReturnCallback<boolean> = (e, overrided) => {
            if(!e)
                this.emit('rename', ctx, pathFrom, { newName, overrided })
            callbackFinal(e, overrided);
        }
        
        this.emit('before-rename', ctx, pathFrom, { newName })

        issuePrivilegeCheck(this, ctx, pathFrom, [ 'canRead', 'canWrite' ], callback, () => {
            this.isLocked(ctx, pathFrom, (e, isLocked) => {
                if(e || isLocked)
                    return callback(e ? e : Errors.Locked);
                
                if(pathFrom.isRoot())
                {
                    this.getFullPath(ctx, (e, fullPath) => {
                        if(fullPath.isRoot())
                            return callback(Errors.InvalidOperation);
                        
                        const newPath = fullPath.getParent().getChildPath(newName);
                        issuePrivilegeCheck(this, ctx, newPath, 'canWrite', callback, () => {
                            ctx.server.getFileSystem(newPath, (fs, _, subPath) => {
                                const go = (overwritten : boolean) =>
                                {
                                    ctx.server.setFileSystem(newPath, this, (successed) => {
                                        if(!successed)
                                            return callback(Errors.InvalidOperation);
                                        
                                        ctx.server.removeFileSystem(fullPath, () => callback(null, overwritten));
                                    })
                                }

                                if(!subPath.isRoot())
                                {
                                    go(false);
                                }
                                else if(!overwrite)
                                {
                                    callback(Errors.ResourceAlreadyExists);
                                }
                                else
                                {
                                    ctx.server.removeFileSystem(newPath, () => {
                                        go(true);
                                    })
                                }
                            })
                        })
                    })
                }
                else
                {
                    this.fastExistCheckEx(ctx, pathFrom, callback, () => {
                    this.fastExistCheckExReverse(ctx, pathFrom.getParent().getChildPath(newName), callback, () => {
                        const newPath = pathFrom.getParent().getChildPath(newName);
                        this.isLocked(ctx, newPath, (e, isLocked) => {
                            if(e || isLocked)
                                return callback(e ? e : Errors.Locked);
                            
                            issuePrivilegeCheck(this, ctx, newPath, 'canWrite', callback, () => {
                                if(this._rename)
                                {
                                    this._rename(pathFrom, newName, {
                                        context: ctx,
                                        destinationPath: newPath
                                    }, callback);
                                }
                                else
                                {
                                    this.move(ctx, pathFrom, pathFrom.getParent().getChildPath(newName), overwrite, callback);
                                }
                            })
                        })
                    })
                    })
                }
            })
        })
    }
    protected _rename?(pathFrom : Path, newName : string, ctx : RenameInfo, callback : ReturnCallback<boolean>) : void

    /**
     * Get the mime type and the encoding of the resource's content.
     * By default, it uses the file name of the resource to determine its mime type and its encoding.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     */
    mimeTypeAsync(ctx : RequestContext, path : Path | string) : Promise<string>
    /**
     * Get the mime type and the encoding of the resource's content.
     * By default, it uses the file name of the resource to determine its mime type and its encoding.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param targetSource Define if the content must be the source or the computed content. Might make no difference depending on the implementation.
     */
    mimeTypeAsync(ctx : RequestContext, path : Path | string, targetSource : boolean) : Promise<string>
    mimeTypeAsync(ctx : RequestContext, path : Path | string, targetSource ?: boolean) : Promise<string>
    {
        return promisifyCall((cb) => this.mimeType(ctx, path, targetSource, cb))
    }

    /**
     * Get the mime type and the encoding of the resource's content.
     * By default, it uses the file name of the resource to determine its mime type and its encoding.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param callback Returns the mime type and the encoding of the resource.
     */
    mimeType(ctx : RequestContext, path : Path | string, callback : ReturnCallback<string>) : void
    /**
     * Get the mime type and the encoding of the resource's content.
     * By default, it uses the file name of the resource to determine its mime type and its encoding.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param targetSource Define if the content must be the source or the computed content. Might make no difference depending on the implementation.
     * @param callback Returns the mime type and the encoding of the resource.
     */
    mimeType(ctx : RequestContext, path : Path | string, targetSource : boolean, callback : ReturnCallback<string>) : void
    mimeType(ctx : RequestContext, _path : Path | string, _targetSource : boolean | ReturnCallback<string>, _callback ?: ReturnCallback<string>) : void
    {
        const targetSource = ensureValue(_callback ? _targetSource as boolean : undefined, true);
        const callback = _callback ? _callback : _targetSource as ReturnCallback<string>;
        const path = new Path(_path);

        issuePrivilegeCheck(this, ctx, path, targetSource ? 'canReadContentSource' : 'canReadContentTranslated', callback, () => {
            this.fastExistCheckEx(ctx, path, callback, () => {
                if(this._mimeType)
                {
                    this._mimeType(path, {
                        context: ctx,
                        targetSource
                    }, callback);
                }
                else
                {
                    StandardMethods.standardMimeType(ctx, this, path, targetSource, callback);
                }
            })
        })
    }
    protected _mimeType?(path : Path, ctx : MimeTypeInfo, callback : ReturnCallback<string>) : void

    /**
     * Get the size of the resource's content.
     * If the '_size' method is not implemented, it returns 'undefined'.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     */
    sizeAsync(ctx : RequestContext, path : Path | string) : Promise<number>
    /**
     * Get the size of the resource's content.
     * If the '_size' method is not implemented, it returns 'undefined'.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param targetSource Define if the content must be the source or the computed content. Might make no difference depending on the implementation.
     */
    sizeAsync(ctx : RequestContext, path : Path | string, targetSource : boolean) : Promise<number>
    sizeAsync(ctx : RequestContext, path : Path | string, targetSource ?: boolean) : Promise<number>
    {
        return promisifyCall((cb) => this.size(ctx, path, targetSource, cb))
    }

    /**
     * Get the size of the resource's content.
     * If the '_size' method is not implemented, it returns 'undefined'.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param callback Returns the size of the resource.
     */
    size(ctx : RequestContext, path : Path | string, callback : ReturnCallback<number>) : void
    /**
     * Get the size of the resource's content.
     * If the '_size' method is not implemented, it returns 'undefined'.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param targetSource Define if the content must be the source or the computed content. Might make no difference depending on the implementation.
     * @param callback Returns the size of the resource.
     */
    size(ctx : RequestContext, path : Path | string, targetSource : boolean, callback : ReturnCallback<number>) : void
    size(ctx : RequestContext, path : Path | string, _targetSource : boolean | ReturnCallback<number>, _callback ?: ReturnCallback<number>) : void
    {
        const targetSource = ensureValue(_callback ? _targetSource as boolean : undefined, false);
        const callback = _callback ? _callback : _targetSource as ReturnCallback<number>;
        const pPath = new Path(path);

        issuePrivilegeCheck(this, ctx, pPath, targetSource ? 'canReadContentSource' : 'canReadContentTranslated', callback, () => {
            this.fastExistCheckEx(ctx, pPath, callback, () => {
                if(!this._size)
                    return callback(null, undefined);

                this._size(pPath, {
                    context: ctx,
                    targetSource
                }, callback);
            })
        })
    }
    protected _size?(path : Path, ctx : SizeInfo, callback : ReturnCallback<number>) : void

    /**
     * Get the list of available lock kinds.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     */
    availableLocksAsync(ctx : RequestContext, path : Path | string) : Promise<LockKind[]>
    {
        return promisifyCall((cb) => this.availableLocks(ctx, path, cb))
    }

    /**
     * Get the list of available lock kinds.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param callback Returns the list of available lock kinds.
     */
    availableLocks(ctx : RequestContext, path : Path | string, callback : ReturnCallback<LockKind[]>) : void
    {
        const pPath = new Path(path);

        issuePrivilegeCheck(this, ctx, pPath, 'canReadLocks', callback, () => {
            this.fastExistCheckEx(ctx, pPath, callback, () => {
                if(!this._availableLocks)
                {
                    callback(null, [
                        new LockKind(LockScope.Exclusive, LockType.Write),
                        new LockKind(LockScope.Shared, LockType.Write)
                    ]);
                }
                else
                {
                    this._availableLocks(pPath, {
                        context: ctx
                    }, callback);
                }
            })
        })
    }
    protected _availableLocks?(path : Path, ctx : AvailableLocksInfo, callback : ReturnCallback<LockKind[]>) : void

    /**
     * Get the lock manager of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     */
    lockManagerAsync(ctx : RequestContext, path : Path | string) : Promise<ILockManagerAsync>
    {
        return promisifyCall((cb) => this.lockManager(ctx, path, cb))
    }

    /**
     * Get the lock manager of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param callback Returns the lock manager of the resource.
     */
    lockManager(ctx : RequestContext, path : Path | string, callback : ReturnCallback<ILockManagerAsync>) : void
    {
        const pPath = new Path(path);

        this.fastExistCheckEx(ctx, pPath, callback, () => {
            this._lockManager(pPath, {
                context: ctx
            }, (e, lm) => {
                if(e)
                    return callback(e);
                
                const buffIsLocked = new BufferedIsLocked(this, ctx, pPath);
                const fs = this;
                const manager = {
                    getLocksAsync() : Promise<Lock[]>
                    {
                        return promisifyCall((cb) => manager.getLocks(cb));
                    },
                    getLocks(callback : ReturnCallback<Lock[]>) : void
                    {
                        issuePrivilegeCheck(fs, ctx, pPath, 'canReadLocks', callback, () => {
                            lm.getLocks(callback);
                        })
                    },
                    setLockAsync(lock : Lock) : Promise<void>
                    {
                        return promisifyCall((cb) => manager.setLock(lock, cb));
                    },
                    setLock(lock : Lock, callback : SimpleCallback) : void
                    {
                        fs.emit('before-lock-set', ctx, pPath, { lock });
                        issuePrivilegeCheck(fs, ctx, pPath, 'canWriteLocks', callback, () => {
                            buffIsLocked.isLocked((e, isLocked) => {
                                if(e || isLocked)
                                    return callback(e ? e : Errors.Locked);
                                
                                lm.setLock(lock, (e) => {
                                    if(!e)
                                        fs.emit('lock-set', ctx, pPath, { lock });
                                    callback(e);
                                });
                            })
                        })
                    },
                    removeLockAsync(uuid : string) : Promise<boolean>
                    {
                        return promisifyCall((cb) => manager.removeLock(uuid, cb));
                    },
                    removeLock(uuid : string, callback : ReturnCallback<boolean>) : void
                    {
                        fs.emit('before-lock-remove', ctx, pPath, { uuid });
                        issuePrivilegeCheck(fs, ctx, pPath, 'canWriteLocks', callback, () => {
                            buffIsLocked.isLocked((e, isLocked) => {
                                if(e || isLocked)
                                    return callback(e ? e : Errors.Locked);
                                
                                lm.removeLock(uuid, (e, removed) => {
                                    if(!e)
                                        fs.emit('lock-remove', ctx, pPath, { uuid, removed });
                                    callback(e, removed);
                                });
                            })
                        })
                    },
                    getLockAsync(uuid : string) : Promise<Lock>
                    {
                        return promisifyCall((cb) => manager.getLock(uuid, cb));
                    },
                    getLock(uuid : string, callback : ReturnCallback<Lock>) : void
                    {
                        issuePrivilegeCheck(fs, ctx, pPath, 'canReadLocks', callback, () => {
                            lm.getLock(uuid, callback);
                        })
                    },
                    refreshAsync(uuid : string, timeoutSeconds : number) : Promise<Lock>
                    {
                        return promisifyCall((cb) => manager.refresh(uuid, timeoutSeconds, cb));
                    },
                    refresh(uuid : string, timeoutSeconds : number, callback : ReturnCallback<Lock>) : void
                    {
                        fs.emit('before-lock-refresh', ctx, pPath, { uuid, timeout: timeoutSeconds });
                        issuePrivilegeCheck(fs, ctx, pPath, 'canWriteLocks', callback, () => {
                            buffIsLocked.isLocked((e, isLocked) => {
                                if(e || isLocked)
                                    return callback(e ? e : Errors.Locked);
                                
                                lm.refresh(uuid, timeoutSeconds, (e, lock) => {
                                    if(!e)
                                        fs.emit('lock-refresh', ctx, pPath, { uuid, timeout: timeoutSeconds, lock });
                                    callback(e, lock);
                                });
                            })
                        })
                    }
                };

                callback(null, manager)
            });
        })
    }
    protected abstract _lockManager(path : Path, ctx : LockManagerInfo, callback : ReturnCallback<ILockManager>) : void

    /**
     * Get the property manager of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     */
    propertyManagerAsync(ctx : RequestContext, path : Path | string) : Promise<IPropertyManager>
    {
        return promisifyCall((cb) => this.propertyManager(ctx, path, cb))
    }

    /**
     * Get the property manager of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param callback Returns the property manager of the resource.
     */
    propertyManager(ctx : RequestContext, path : Path | string, callback : ReturnCallback<IPropertyManager>) : void
    {
        const pPath = new Path(path);

        this.fastExistCheckEx(ctx, pPath, callback, () => {
            this._propertyManager(pPath, {
                context: ctx
            }, (e, pm) => {
                if(e)
                    return callback(e);
                
                const buffIsLocked = new BufferedIsLocked(this, ctx, pPath);
                const fs = this;
                
                callback(null, {
                    setProperty(name : string, value : ResourcePropertyValue, attributes : PropertyAttributes, callback : SimpleCallback) : void
                    {
                        fs.emit('before-property-set', ctx, pPath, { name, value, attributes });
                        issuePrivilegeCheck(fs, ctx, pPath, 'canWriteProperties', callback, () => {
                            buffIsLocked.isLocked((e, isLocked) => {
                                if(e || isLocked)
                                    return callback(e ? e : Errors.Locked);
                                
                                pm.setProperty(name, value, attributes, (e) => {
                                    if(!e)
                                        fs.emit('property-set', ctx, pPath, { name, value, attributes });
                                    callback(e);
                                });
                            })
                        })
                    },
                    getProperty(name : string, callback : Return2Callback<ResourcePropertyValue, PropertyAttributes>) : void
                    {
                        issuePrivilegeCheck(fs, ctx, pPath, 'canReadProperties', callback, () => {
                            pm.getProperty(name, callback);
                        })
                    },
                    removeProperty(name : string, callback : SimpleCallback) : void
                    {
                        fs.emit('before-property-remove', ctx, pPath, { name });
                        issuePrivilegeCheck(fs, ctx, pPath, 'canWriteProperties', callback, () => {
                            buffIsLocked.isLocked((e, isLocked) => {
                                if(e || isLocked)
                                    return callback(e ? e : Errors.Locked);
                                
                                pm.removeProperty(name, (e) => {
                                    if(!e)
                                        fs.emit('property-remove', ctx, pPath, { name });
                                    callback(e);
                                });
                            })
                        })
                    },
                    getProperties(callback : ReturnCallback<PropertyBag>, byCopy ?: boolean) : void
                    {
                        issuePrivilegeCheck(fs, ctx, pPath, 'canReadProperties', callback, () => {
                            pm.getProperties((e, bag) => {
                                if(!bag)
                                    return callback(e, bag);

                                ctx.server.options.storageManager.available(ctx, this, (availableSize) => {
                                    if(availableSize === -1)
                                        return callback(e, bag);

                                    ctx.server.options.storageManager.reserved(ctx, this, (reservedSize) => {
                                        bag['DAV:quota-available-bytes'] = {
                                            value: availableSize.toString()
                                        };
                                        bag['DAV:quota-used-bytes'] = {
                                            value: reservedSize.toString()
                                        };
                                        callback(e, bag);
                                    })
                                })
                            }, byCopy);
                        })
                    }
                })
            });
        })
    }
    protected abstract _propertyManager(path : Path, ctx : PropertyManagerInfo, callback : ReturnCallback<IPropertyManager>) : void

    /**
     * Get the list of children of a resource.
     * Excludes the external resources, such as file systems mounted as child.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     */
    readDirAsync(ctx : RequestContext, path : Path | string) : Promise<string[]>
    /**
     * Get the list of children of a resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param retrieveExternalFiles Define if it must include the resources out of the file system, like other file systems mounted as child.
     */
    readDirAsync(ctx : RequestContext, path : Path | string, retrieveExternalFiles : boolean) : Promise<string[]>
    readDirAsync(ctx : RequestContext, path : Path | string, retrieveExternalFiles ?: boolean) : Promise<string[]>
    {
        return promisifyCall((cb) => this.readDir(ctx, path, retrieveExternalFiles, cb));
    }

    /**
     * Get the list of children of a resource.
     * Excludes the external resources, such as file systems mounted as child.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param callback Returns the list of children (file name) of the resource.
     */
    readDir(ctx : RequestContext, path : Path | string, callback : ReturnCallback<string[]>) : void
    /**
     * Get the list of children of a resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param retrieveExternalFiles Define if it must include the resources out of the file system, like other file systems mounted as child.
     * @param callback Returns the list of children (file name) of the resource.
     */
    readDir(ctx : RequestContext, path : Path | string, retrieveExternalFiles : boolean, callback : ReturnCallback<string[]>) : void
    readDir(ctx : RequestContext, path : Path | string, _retrieveExternalFiles : boolean | ReturnCallback<string[]>, _callback ?: ReturnCallback<string[]>) : void
    {
        const retrieveExternalFiles = ensureValue(_callback ? _retrieveExternalFiles as boolean : undefined, false);
        const __callback = _callback ? _callback : _retrieveExternalFiles as ReturnCallback<string[]>;
        const pPath = new Path(path);
        const callback = (e ?: Error, data ?: Path[]) => {
            if(e)
                return __callback(e);
            if(!data)
                data = [];
            
            this.getFullPath(ctx, (e, fsFullPath) => {
                new Workflow()
                    .each(data, (path, cb) => {
                        this.checkPrivilege(ctx, path, 'canReadProperties', (e, can) => {
                            if(e)
                                cb(e);
                            else
                                cb(null, can ? path : null);
                        });
                    })
                    .error(__callback)
                    .done(() => __callback(null, data.filter((p) => !!p).map((p) => p.fileName())));
            })
        }

        issuePrivilegeCheck(this, ctx, pPath, 'canReadProperties', callback, () => {
            this.fastExistCheckEx(ctx, pPath, callback, () => {
                const next = (base : Path[]) => {
                    if(!this._readDir)
                        return callback(null, base);
                    
                    this._readDir(pPath, {
                        context: ctx
                    }, (e, paths) => {
                        if(e)
                            return callback(e);
                        
                        if(paths.length === 0)
                            return callback(null, base);
                        
                        if(paths[0].constructor === String)
                            base = base.concat((paths as string[]).map((s) => pPath.getChildPath(s)));
                        else
                            base = base.concat(paths as Path[]);
                        
                        callback(null, base);
                    });
                }

                if(!retrieveExternalFiles)
                    return next([]);

                this.getFullPath(ctx, (e, thisFullPath) => {
                    if(e)
                        return callback(e);
                    
                    ctx.server.getChildFileSystems(thisFullPath.getChildPath(pPath), (fss) => {
                        this.localize(ctx, fss.map((f) => f.path), (e, paths) => {
                            if(e)
                                return callback(e);
                            next(paths);
                        })
                    })
                })
            })
        })
    }
    protected _readDir?(path : Path, ctx : ReadDirInfo, callback : ReturnCallback<string[] | Path[]>) : void

    protected static neutralizeEmptyDate(date : number, defaultDate ?: number) : number
    {
        if(!date || isNaN(date))
        {
            if(defaultDate === undefined || defaultDate === null)
                defaultDate = 0;
            return defaultDate;
        }
        else
        {
            return date;
        }
    }
    protected static neutralizeEmptyDateCallback = (callback : ReturnCallback<number>) : ReturnCallback<number> => {
        return (e : Error, date : number) => {
            callback(e, FileSystem.neutralizeEmptyDate(date));
        }
    }

    /**
     * Get the creation date information of a resource.
     * If neither '_creationDate' nor '_lastModifiedDate' are implemented, it returns 0.
     * If '_creationDate' is not implemented, it calls the 'lastModifiedDate' method.
     * Otherwise it calls the '_creationDate' method.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     */
    creationDateAsync(ctx : RequestContext, path : Path | string) : Promise<number>
    {
        return promisifyCall((cb) => this.creationDate(ctx, path, cb));
    }

    /**
     * Get the creation date information of a resource.
     * If neither '_creationDate' nor '_lastModifiedDate' are implemented, it returns 0.
     * If '_creationDate' is not implemented, it calls the 'lastModifiedDate' method.
     * Otherwise it calls the '_creationDate' method.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param callback Returns the creation date of the resource.
     */
    creationDate(ctx : RequestContext, path : Path | string, callback : ReturnCallback<number>) : void
    {
        const pPath = new Path(path);
        callback = FileSystem.neutralizeEmptyDateCallback(callback);

        issuePrivilegeCheck(this, ctx, pPath, 'canReadProperties', callback, () => {
            this.fastExistCheckEx(ctx, pPath, callback, () => {
                if(!this._creationDate && !this._lastModifiedDate)
                    return callback(null, 0);
                if(!this._creationDate)
                    return this.lastModifiedDate(ctx, pPath, callback);
                
                this._creationDate(pPath, {
                    context: ctx
                }, callback);
            })
        })
    }
    protected _creationDate?(path : Path, ctx : CreationDateInfo, callback : ReturnCallback<number>) : void

    /**
     * Get the last modified date information of a resource.
     * If neither '_creationDate' nor '_lastModifiedDate' are implemented, it returns 0.
     * If '_lastModifiedDate' is not implemented, it calls the 'creationDate' method.
     * Otherwise it calls the '_lastModifiedDate' method.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     */
    lastModifiedDateAsync(ctx : RequestContext, path : Path | string) : Promise<number>
    {
        return promisifyCall((cb) => this.lastModifiedDate(ctx, path, cb));
    }

    /**
     * Get the last modified date information of a resource.
     * If neither '_creationDate' nor '_lastModifiedDate' are implemented, it returns 0.
     * If '_lastModifiedDate' is not implemented, it calls the 'creationDate' method.
     * Otherwise it calls the '_lastModifiedDate' method.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param callback Returns the last modified date of the resource.
     */
    lastModifiedDate(ctx : RequestContext, path : Path | string, callback : ReturnCallback<number>) : void
    {
        const pPath = new Path(path);
        callback = FileSystem.neutralizeEmptyDateCallback(callback);

        issuePrivilegeCheck(this, ctx, pPath, 'canReadProperties', callback, () => {
            this.fastExistCheckEx(ctx, pPath, callback, () => {
                if(!this._creationDate && !this._lastModifiedDate)
                    return callback(null, 0);
                if(!this._lastModifiedDate)
                    return this.creationDate(ctx, pPath, callback);
                
                this._lastModifiedDate(pPath, {
                    context: ctx
                }, callback);
            })
        })
    }
    protected _lastModifiedDate?(path : Path, ctx : LastModifiedDateInfo, callback : ReturnCallback<number>) : void

    /**
     * Get the name of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     */
    webNameAsync(ctx : RequestContext, path : Path | string) : Promise<string>
    {
        return promisifyCall((cb) => this.webName(ctx, path, cb));
    }

    /**
     * Get the name of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param callback Returns the name of the resource.
     */
    webName(ctx : RequestContext, path : Path | string, callback : ReturnCallback<string>) : void
    {
        const pPath = new Path(path);

        issuePrivilegeCheck(this, ctx, pPath, 'canReadProperties', callback, () => {
            this.fastExistCheckEx(ctx, pPath, callback, () => {
                if(pPath.isRoot())
                    this.getFullPath(ctx, (e, pPath) => callback(e, e ? null : pPath.fileName()));
                else
                    callback(null, pPath.fileName());
            })
        })
    }

    /**
     * Get the 'displayName' information of the resource.
     * This value is used in the 'DAV:displayName' tag in the PROPFIND response body.
     * Its default behaviour is to return the result of the 'webName' method. This behaviour can be overrided by implementing the '_displayName' method.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     */
    displayNameAsync(ctx : RequestContext, path : Path | string) : Promise<string>
    {
        return promisifyCall((cb) => this.displayName(ctx, path, cb));
    }

    /**
     * Get the 'displayName' information of the resource.
     * This value is used in the 'DAV:displayName' tag in the PROPFIND response body.
     * Its default behaviour is to return the result of the 'webName' method. This behaviour can be overrided by implementing the '_displayName' method.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param callback Returns the 'displayName' information of the resource.
     */
    displayName(ctx : RequestContext, path : Path | string, callback : ReturnCallback<string>) : void
    {
        const pPath = new Path(path);

        issuePrivilegeCheck(this, ctx, pPath, 'canReadProperties', callback, () => {
            this.fastExistCheckEx(ctx, pPath, callback, () => {
                if(!this._displayName)
                    return this.webName(ctx, pPath, callback);
                
                this._displayName(pPath, {
                    context: ctx
                }, callback);
            })
        })
    }
    protected _displayName?(path : Path, ctx : DisplayNameInfo, callback : ReturnCallback<string>) : void

    /**
     * Get the type of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     */
    typeAsync(ctx : RequestContext, path : Path | string) : Promise<ResourceType>
    {
        return promisifyCall((cb) => this.type(ctx, path, cb));
    }

    /**
     * Get the type of the resource.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param callback Returns the type of the resource.
     */
    type(ctx : RequestContext, path : Path | string, callback : ReturnCallback<ResourceType>) : void
    {
        const pPath = new Path(path);

        issuePrivilegeCheck(this, ctx, pPath, 'canReadProperties', callback, () => {
            this.fastExistCheckEx(ctx, pPath, callback, () => {
                this._type(pPath, {
                    context: ctx
                }, callback);
            })
        })
    }
    protected abstract _type(path : Path, ctx : TypeInfo, callback : ReturnCallback<ResourceType>) : void

    /**
     * Add a sub-tree to the file system at the root.
     * 
     * @param ctx Context of the operation.
     * @param subTree Sub-tree to add.
     */
    addSubTreeAsync(ctx : RequestContext, subTree : SubTree) : Promise<void>
    /**
     * Add a resource to the file system as root.
     * 
     * This method is equivalent to the 'fs.create(ctx, '/', resourceType, callback)' method.
     * 
     * @param ctx Context of the operation.
     * @param resourceType Type of the resource to add.
     */
    addSubTreeAsync(ctx : RequestContext, resourceType : ResourceType | string | Buffer) : Promise<void>
    /**
     * Add a sub-tree to the file system.
     * 
     * @param ctx Context of the operation.
     * @param rootPath Path to which add the sub-tree.
     * @param subTree Sub-tree to add.
     */
    addSubTreeAsync(ctx : RequestContext, rootPath : Path | string, subTree : SubTree) : Promise<void>
    /**
     * Add a resource to the file system.
     * 
     * This method is equivalent to the 'fs.create(ctx, rootPath, resourceType, callback)' method.
     * 
     * @param ctx Context of the operation.
     * @param rootPath Path to which add the resource.
     * @param resourceType Type of the resource to add.
     */
    addSubTreeAsync(ctx : RequestContext, rootPath : Path | string, resourceType : ResourceType | string | Buffer) : Promise<void>
    addSubTreeAsync(ctx : RequestContext, rootPath : any, tree ?: any) : Promise<void>
    {
        return promisifyCall((cb) => this.addSubTree(ctx, rootPath, tree, cb))
    }

    /**
     * Add a sub-tree to the file system at the root.
     * 
     * @param ctx Context of the operation.
     * @param subTree Sub-tree to add.
     * @param callback Returns an error if one occured.
     */
    addSubTree(ctx : RequestContext, subTree : SubTree, callback : SimpleCallback) : void
    /**
     * Add a resource to the file system as root.
     * 
     * This method is equivalent to the 'fs.create(ctx, '/', resourceType, callback)' method.
     * 
     * @param ctx Context of the operation.
     * @param resourceType Type of the resource to add.
     * @param callback Returns an error if one occured.
     */
    addSubTree(ctx : RequestContext, resourceType : ResourceType | string | Buffer, callback : SimpleCallback) : void
    /**
     * Add a sub-tree to the file system.
     * 
     * @param ctx Context of the operation.
     * @param rootPath Path to which add the sub-tree.
     * @param subTree Sub-tree to add.
     * @param callback Returns an error if one occured.
     */
    addSubTree(ctx : RequestContext, rootPath : Path | string, subTree : SubTree, callback : SimpleCallback) : void
    /**
     * Add a resource to the file system.
     * 
     * This method is equivalent to the 'fs.create(ctx, rootPath, resourceType, callback)' method.
     * 
     * @param ctx Context of the operation.
     * @param rootPath Path to which add the resource.
     * @param resourceType Type of the resource to add.
     * @param callback Returns an error if one occured.
     */
    addSubTree(ctx : RequestContext, rootPath : Path | string, resourceType : ResourceType | string | Buffer, callback : SimpleCallback) : void
    addSubTree(ctx : RequestContext, _rootPath : Path | string | SubTree | ResourceType | SimpleCallback | string | Buffer, _tree : SubTree | ResourceType | SimpleCallback | string | Buffer, _callback ?: SimpleCallback) : void
    {
        const _rootPathIsPath = Path.isPath(_rootPath);
        const tree = _rootPathIsPath ? _tree as SubTree | ResourceType : _rootPath as SubTree | ResourceType;
        const rootPath = _rootPathIsPath ? new Path(_rootPath as Path | string) : new Path('/');
        let callback = _callback ? _callback : _tree as SimpleCallback;
        callback = callback ? callback : () => {};

        if(tree.constructor === ResourceType)
        {
            this.create(ctx, rootPath, tree as ResourceType, callback);
        }
        else if(tree.constructor === String || tree.constructor === Buffer)
        {
            const data : String | Buffer = tree as any;
            this.openWriteStream(ctx, rootPath, 'mustCreate', true, data.length, (e, w, created) => {
                if(e)
                    return callback(e);

                w.end(data);
                w.on('error', (e) => {
                    callback(e);
                })
                w.on('finish', () => {
                    callback();
                })
            })
        }
        else
        {
            new Workflow()
                .each(Object.keys(tree), (name, cb) => {
                    const value = tree[name];
                    const childPath = rootPath.getChildPath(name);
                    if(value.constructor === ResourceType || value.constructor === String || value.constructor === Buffer)
                    {
                        this.addSubTree(ctx, childPath, value, cb)
                    }
                    else
                    {
                        this.addSubTree(ctx, childPath, ResourceType.Directory, (e) => {
                            if(e)
                                return cb(e);
                                
                            this.addSubTree(ctx, childPath, value, cb);
                        })
                    }
                })
                .error(callback)
                .done((_) => callback());
        }
    }

    /**
     * Search for locks in the parents, starting at the 'startPath' path.
     * 
     * @param ctx Context of the operation.
     * @param startPath Path where to start the research of locks.
     */
    listDeepLocksAsync(ctx : RequestContext, startPath : Path | string) : Promise<{ [path : string] : Lock[] }>
    /**
     * Search for locks in the parents, starting at the 'startPath' path.
     * 
     * @param ctx Context of the operation.
     * @param startPath Path where to start the research of locks.
     * @param depth Depth to filter out-of-range locks (default = 0) (Infinite = -1).
     */
    listDeepLocksAsync(ctx : RequestContext, startPath : Path | string, depth : number) : Promise<{ [path : string] : Lock[] }>
    listDeepLocksAsync(ctx : RequestContext, startPath : Path | string, depth ?: number) : Promise<{ [path : string] : Lock[] }>
    {
        return promisifyCall((cb) => this.listDeepLocks(ctx, startPath, depth, cb))
    }

    /**
     * Search for locks in the parents, starting at the 'startPath' path.
     * 
     * @param ctx Context of the operation.
     * @param startPath Path where to start the research of locks.
     * @param callback Returns an object { path: lock[] }.
     */
    listDeepLocks(ctx : RequestContext, startPath : Path | string, callback : ReturnCallback<{ [path : string] : Lock[] }>)
    /**
     * Search for locks in the parents, starting at the 'startPath' path.
     * 
     * @param ctx Context of the operation.
     * @param startPath Path where to start the research of locks.
     * @param depth Depth to filter out-of-range locks (default = 0) (Infinite = -1).
     * @param callback Returns an object { path: lock[] }.
     */
    listDeepLocks(ctx : RequestContext, startPath : Path | string, depth : number, callback : ReturnCallback<{ [path : string] : Lock[] }>)
    listDeepLocks(ctx : RequestContext, startPath : Path | string, _depth : number | ReturnCallback<{ [path : string] : Lock[] }>, _callback ?: ReturnCallback<{ [path : string] : Lock[] }>)
    {
        const depth = ensureValue(_callback ? _depth as number : undefined, 0);
        const callback = _callback ? _callback : _depth as ReturnCallback<{ [path : string] : Lock[] }>;
        const pStartPath = new Path(startPath);
        
        this.lockManager(ctx, pStartPath, (e, lm) => {
            if(e === Errors.ResourceNotFound)
            {
                lm = {
                    getLocks(callback : ReturnCallback<Lock[]>) : void
                    {
                        callback(null, []);
                    }
                } as ILockManagerAsync;
            }
            else if(e)
            {
                return callback(e);
            }
            
            lm.getLocks((e, locks) => {
                if(e === Errors.NotEnoughPrivilege)
                {
                    locks = [];
                }
                else if(e)
                {
                    return callback(e);
                }
                
                if(depth !== -1)
                    locks = locks.filter((f) => f.depth === -1 || f.depth >= depth);
                
                const go = (fs : FileSystem, parentPath : Path) =>
                {
                    const destDepth = depth === -1 ? -1 : depth + 1;
                    fs.listDeepLocks(ctx, parentPath, destDepth, (e, pLocks) => {
                        if(e)
                            return callback(e);
                        
                        if(locks && locks.length > 0)
                            pLocks[pStartPath.toString()] = locks;
                        callback(null, pLocks);
                    })
                }

                if(!pStartPath.isRoot())
                {
                    go(this, pStartPath.getParent());
                }
                else
                {
                    this.getFullPath(ctx, (e, fsPath) => {
                        if(e)
                            return callback(e);
                        
                        if(fsPath.isRoot())
                        {
                            const result = {};
                            if(locks && locks.length > 0)
                                result[pStartPath.toString()] = locks;
                            return callback(null, result);
                        }
                        
                        ctx.server.getFileSystem(fsPath.getParent(), (fs, _, subPath) => {
                            go(fs, subPath);
                        })
                    })
                }
            })
        })
    }

    /**
     * Get the root based file system path. This can also be understood as getting the mount path of the file system.
     * 
     * @param ctx Context of the operation.
     */
    getFullPathAsync(ctx : RequestContext) : Promise<Path>
    /**
     * Get the root based path.
     * 
     * @example If the file system is mounted on '/folder1', resolving '/folder2/folder3' will result to '/folder1/folder2/folder3'.
     * 
     * @param ctx Context of the operation.
     * @param path Path to resolve.
     */
    getFullPathAsync(ctx : RequestContext, path : Path | string) : Promise<Path>
    getFullPathAsync(ctx : RequestContext, path ?: Path | string) : Promise<Path>
    {
        return promisifyCall((cb) => this.getFullPath(ctx, path, cb));
    }

    /**
     * Get the root based file system path. This can also be understood as getting the mount path of the file system.
     * 
     * @param ctx Context of the operation.
     * @param callback Returns the full path (root based).
     */
    getFullPath(ctx : RequestContext, callback : ReturnCallback<Path>) : void
    /**
     * Get the root based path.
     * 
     * @example If the file system is mounted on '/folder1', resolving '/folder2/folder3' will result to '/folder1/folder2/folder3'.
     * 
     * @param ctx Context of the operation.
     * @param path Path to resolve.
     * @param callback Returns the root based path.
     */
    getFullPath(ctx : RequestContext, path : Path | string, callback : ReturnCallback<Path>) : void
    getFullPath(ctx : RequestContext, _path : Path | string | ReturnCallback<Path>, _callback ?: ReturnCallback<Path>) : void
    {
        const path = !_path || typeof _path === 'function' ? undefined : new Path(_path as Path | string);
        const callback = _callback ? _callback : _path as ReturnCallback<Path>;
        
        ctx.server.getFileSystemPath(this, (fsPath) => {
            callback(null, path ? fsPath.getChildPath(path) : fsPath);
        })
    }

    /**
     * From the global paths (root based), retrieve the file system local paths (file system based).
     * 
     * @example If the file system is mounted on '/folder1', the path '/folder1/folder2/folder3' will be returned as '/folder2/folder3'.
     * @param ctx Context of the operation.
     * @param fullPath The path or the list of paths to localize in the file system.
     */
    localizeAsync(ctx : RequestContext, fullPath : Path) : Promise<Path[]>
    localizeAsync(ctx : RequestContext, fullPath : Path[]) : Promise<Path[]>
    localizeAsync(ctx : RequestContext, fullPath : string) : Promise<Path[]>
    localizeAsync(ctx : RequestContext, fullPath : string[]) : Promise<Path[]>
    localizeAsync(ctx : RequestContext, fullPath : (string | Path)[]) : Promise<Path[]>
    localizeAsync(ctx : RequestContext, fullPath : Path | string | (string | Path)[]) : Promise<Path[]>
    {
        return promisifyCall((cb) => this.localize(ctx, fullPath as any, cb));
    }

    /**
     * From the global paths (root based), retrieve the file system local paths (file system based).
     * 
     * @example If the file system is mounted on '/folder1', the path '/folder1/folder2/folder3' will be returned as '/folder2/folder3'.
     * @param ctx Context of the operation.
     * @param fullPath The path or the list of paths to localize in the file system.
     * @param callback Returns the list of local paths.
     */
    localize(ctx : RequestContext, fullPath : Path, callback : ReturnCallback<Path[]>)
    localize(ctx : RequestContext, fullPath : Path[], callback : ReturnCallback<Path[]>)
    localize(ctx : RequestContext, fullPath : string, callback : ReturnCallback<Path[]>)
    localize(ctx : RequestContext, fullPath : string[], callback : ReturnCallback<Path[]>)
    localize(ctx : RequestContext, fullPath : (string | Path)[], callback : ReturnCallback<Path[]>)
    localize(ctx : RequestContext, fullPath : Path | string | (string | Path)[], callback : ReturnCallback<Path[]>)
    {
        this.getFullPath(ctx, (e, fsFullPath) => {
            if(e)
                return callback(e);
            
            const paths = fullPath.constructor === Array ? fullPath as any[] : [ fullPath as any ];

            callback(null, paths
                .map((p) => new Path(p))
                .map((p) => {
                    fsFullPath.paths.forEach(() => p.removeRoot());
                    return p;
                })
            );
        })
    }

    /**
     * Check if the user in the current context (ctx) has ALL privileges requested.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param privilege Privilege or list of privileges to check.
     */
    checkPrivilegeAsync(ctx : RequestContext, path : Path | string, privilege : BasicPrivilege) : Promise<boolean>
    checkPrivilegeAsync(ctx : RequestContext, path : Path | string, privileges : BasicPrivilege[]) : Promise<boolean>
    checkPrivilegeAsync(ctx : RequestContext, path : Path | string, privilege : string) : Promise<boolean>
    checkPrivilegeAsync(ctx : RequestContext, path : Path | string, privileges : string[]) : Promise<boolean>
    checkPrivilegeAsync(ctx : RequestContext, path : Path | string, privileges : BasicPrivilege | BasicPrivilege[]) : Promise<boolean>
    checkPrivilegeAsync(ctx : RequestContext, path : Path | string, privileges : string | string[]) : Promise<boolean>
    checkPrivilegeAsync(ctx : RequestContext, path : Path | string, privileges : string | string[] | BasicPrivilege | BasicPrivilege[]) : Promise<boolean>
    {
        return promisifyCall((cb) => this.checkPrivilege(ctx, path, privileges as any, cb));
    }

    /**
     * Check if the user in the current context (ctx) has ALL privileges requested.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param privilege Privilege or list of privileges to check.
     * @param callback Returns if the current user has ALL of the privileges.
     */
    checkPrivilege(ctx : RequestContext, path : Path | string, privilege : BasicPrivilege, callback : ReturnCallback<boolean>)
    checkPrivilege(ctx : RequestContext, path : Path | string, privileges : BasicPrivilege[], callback : ReturnCallback<boolean>)
    checkPrivilege(ctx : RequestContext, path : Path | string, privilege : string, callback : ReturnCallback<boolean>)
    checkPrivilege(ctx : RequestContext, path : Path | string, privileges : string[], callback : ReturnCallback<boolean>)
    checkPrivilege(ctx : RequestContext, path : Path | string, privileges : BasicPrivilege | BasicPrivilege[], callback : ReturnCallback<boolean>)
    checkPrivilege(ctx : RequestContext, path : Path | string, privileges : string | string[], callback : ReturnCallback<boolean>)
    checkPrivilege(ctx : RequestContext, path : Path | string, privileges : string | string[] | BasicPrivilege | BasicPrivilege[], callback : ReturnCallback<boolean>)
    {
        if(privileges.constructor === String)
            privileges = [ privileges as string ];
        
        this.getFullPath(ctx, path, (e, fullPath) => {
            this.privilegeManager(ctx, path, (e, privilegeManager) => {
                if(e)
                    return callback(e);
                
                const resource = this.resource(ctx, new Path(path));
                privilegeManager.can(fullPath, resource, privileges as string[], callback);
            })
        })
    }

    /**
     * Get the privilege manager to use to authorize actions for a user.
     * By default, it returns the value in the server options, but it can be overrided by implementing the '_privilegeManager' method.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     */
    privilegeManagerAsync(ctx : RequestContext, path : Path | string) : Promise<PrivilegeManager>
    {
        return promisifyCall((cb) => this.privilegeManager(ctx, path, cb));
    }

    /**
     * Get the privilege manager to use to authorize actions for a user.
     * By default, it returns the value in the server options, but it can be overrided by implementing the '_privilegeManager' method.
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param callback Returns the privilege manager representing the requested resource.
     */
    privilegeManager(ctx : RequestContext, path : Path | string, callback : ReturnCallback<PrivilegeManager>)
    {
        if(!this._privilegeManager)
            return callback(null, ctx.server.options.privilegeManager);
        
        this._privilegeManager(new Path(path), {
            context: ctx
        }, callback);
    }
    protected _privilegeManager?(path : Path, info : PrivilegeManagerInfo, callback : ReturnCallback<PrivilegeManager>)

    /**
     * Get if a resource is locked by another user than the one in the context argument or if the user has rights to write to the resource.
     * If the user has locked the resource and there is no conflicting lock, so the resource is considered as "not locked".
     * If the user didn't locked the resource and is not administrator, then the resource is considered as "locked".
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     */
    isLockedAsync(ctx : RequestContext, path : Path | string) : Promise<boolean>
    isLockedAsync(ctx : RequestContext, path : Path | string, depth : number) : Promise<boolean>
    isLockedAsync(ctx : RequestContext, path : Path | string, depth ?: number) : Promise<boolean>
    {
        return promisifyCall((cb) => this.isLockedAsync(ctx, path, depth))
    }

    /**
     * Get if a resource is locked by another user than the one in the context argument or if the user has rights to write to the resource.
     * If the user has locked the resource and there is no conflicting lock, so the resource is considered as "not locked".
     * If the user didn't locked the resource and is not administrator, then the resource is considered as "locked".
     * 
     * @param ctx Context of the operation.
     * @param path Path of the resource.
     * @param callback Returns if the resource is locked or not (true = locked, cannot write to it ; false = not locked) or returns an error.
     */
    isLocked(ctx : RequestContext, path : Path | string, callback : ReturnCallback<boolean>)
    isLocked(ctx : RequestContext, path : Path | string, depth : number, callback : ReturnCallback<boolean>)
    isLocked(ctx : RequestContext, path : Path | string, _depth : number | ReturnCallback<boolean>, _callback ?: ReturnCallback<boolean>)
    {
        const callback = _callback ? _callback : _depth as ReturnCallback<boolean>;
        const depth = typeof _depth === 'number' ? _depth : 0;

        if(ctx.user && ctx.user.isAdministrator)
            return callback(null, false);
        
        const pPath = new Path(path);

        const checkThis = () => {
            this._lockManager(pPath, { context: ctx }, (e, lm) => {
                if(e === Errors.ResourceNotFound)
                    return callback(null, false);
                if(e)
                    return callback(e);

                lm.getLocks((e, locks) => {
                    if(e === Errors.ResourceNotFound)
                        return callback(null, false);
                    if(e)
                        return callback(e);
                    
                    locks = locks.filter((l) => l.depth === -1 || l.depth >= depth);
                    
                    if(!ctx.user)
                        return callback(null, locks.length > 0);

                    if(locks.some((l) => ctx.user.uid !== l.userUid && l.lockKind.scope.isSame(LockScope.Exclusive)))
                        return callback(null, true);
                            
                    let isShared = false;
                    for(const lock of locks)
                    {
                        if(lock.lockKind.scope.isSame(LockScope.Shared))
                        {
                            isShared = true;
                            if(lock.userUid === ctx.user.uid)
                                return callback(null, false);
                        }
                    }

                    callback(null, isShared);
                })
            })
        }
        
        this.getFullPath(ctx, pPath, (e, fullPath) => {
            if(fullPath.isRoot())
                return checkThis();

            ctx.server.getFileSystem(pPath.getParent(), (fs, rootPath, subPath) => {
                fs.isLocked(ctx, subPath, depth + 1, (e, locked) => {
                    if(e || locked)
                        return callback(e, locked);
                    
                    checkThis();
                })
            })
        })
    }

    /**
     * Serialize the file system based on the 'this.serializer()' value.
     */
    serializeAsync() : Promise<any>
    {
        return promisifyCall((cb) => this.serialize(cb))
    }

    /**
     * Serialize the file system based on the 'this.serializer()' value.
     * 
     * @param callback Returns the serialized data or an error.
     */
    serialize(callback : ReturnCallback<any>) : void
    {
        const serializer = this.serializer();
        if(!serializer)
            return callback();
        
        serializer.serialize(this, callback);
    }

    /**
     * Attach a listener to an event.
     * 
     * @param server Server in which the event can happen.
     * @param event Name of the event.
     * @param listener Listener of the event.
     */
    on(server : WebDAVServer, event : FileSystemEvent, listener : (ctx : RequestContext, path : Path, data ?: any) => void) : this
    /**
     * Attach a listener to an event.
     * 
     * @param server Server in which the event can happen.
     * @param event Name of the event.
     * @param listener Listener of the event.
     */
    on(server : WebDAVServer, event : string, listener : (ctx : RequestContext, path : Path, data ?: any) => void) : this
    /**
     * Attach a listener to an event.
     * 
     * @param ctx Context containing the server in which the event can happen.
     * @param event Name of the event.
     * @param listener Listener of the event.
     */
    on(ctx : RequestContext, event : FileSystemEvent, listener : (ctx : RequestContext, path : Path, data ?: any) => void) : this
    /**
     * Attach a listener to an event.
     * 
     * @param ctx Context containing the server in which the event can happen.
     * @param event Name of the event.
     * @param listener Listener of the event.
     */
    on(ctx : RequestContext, event : string, listener : (ctx : RequestContext, path : Path, data ?: any) => void) : this
    on(ctx : RequestContext | WebDAVServer, event : FileSystemEvent, listener : (ctx : RequestContext, path : Path, data ?: any) => void) : this
    {
        const server = (ctx as any).events ? ctx as WebDAVServer : (ctx as RequestContext).server;
        server.on(event, (ctx, fs, path) => {
            if(fs === this)
                listener(ctx, path);
        })
        return this;
    }

    /**
     * Trigger an event.
     * 
     * @param event Name of the event.
     * @param ctx Context of the event.
     * @param path Path of the resource on which the event happened.
     */
    emit(event : string, ctx : RequestContext, path : Path | string, data ?: any) : void
    {
        ctx.server.emit(event, ctx, this, path, data);
    }
}

function issuePrivilegeCheck(fs : FileSystem, ctx : RequestContext, path : Path | string, privilege : BasicPrivilege | BasicPrivilege[], badCallback : SimpleCallback, goodCallback : () => void)
{
    fs.checkPrivilege(ctx, path, privilege, (e, can) => {
        if(e)
            badCallback(e);
        else if(!can)
            badCallback(Errors.NotEnoughPrivilege);
        else
            goodCallback();
    })
}