Skip to content

Custom resources

Adrien Castex edited this page Jun 20, 2017 · 6 revisions

Index

Inheritance

The custom resource MUST implement the IResource interface :

interface IResource
{
    parent : IResource
    fsManager : FSManager

    // ****************************** Actions ****************************** //
    create(callback : SimpleCallback)
    delete(callback : SimpleCallback)
    moveTo(parent : IResource, newName : string, overwrite : boolean, callback : SimpleCallback)
    rename(newName : string, callback : Return2Callback<string, string>)
    
    // ****************************** Content ****************************** //
    write(targetSource : boolean, callback : ReturnCallback<Writable>)
    read(targetSource : boolean, callback : ReturnCallback<Readable>)
    mimeType(targetSource : boolean, callback : ReturnCallback<string>)
    size(targetSource : boolean, callback : ReturnCallback<number>)
    
    // ****************************** Locks ****************************** //
    getLocks(callback : ReturnCallback<Lock[]>)
    setLock(lock : Lock, callback : SimpleCallback)
    removeLock(uuid : string, callback : ReturnCallback<boolean>)
    getAvailableLocks(callback : ReturnCallback<LockKind[]>)
    getLock(uuid : string, callback : ReturnCallback<Lock>)

    // ****************************** Children ****************************** //
    addChild(resource : IResource, callback : SimpleCallback)
    removeChild(resource : IResource, callback : SimpleCallback)
    getChildren(callback : ReturnCallback<IResource[]>)

    // ****************************** Properties ****************************** //
    setProperty(name : string, value : ResourcePropertyValue, callback : SimpleCallback)
    getProperty(name : string, callback : ReturnCallback<ResourcePropertyValue>)
    removeProperty(name : string, callback : SimpleCallback)
    getProperties(callback : ReturnCallback<object>)
    
    // ****************************** Std meta-data ****************************** //
    creationDate(callback : ReturnCallback<number>)
    lastModifiedDate(callback : ReturnCallback<number>)
    webName(callback : ReturnCallback<string>)
    displayName?(callback : ReturnCallback<string>)
    type(callback : ReturnCallback<ResourceType>)
    
    // ****************************** Gateway ****************************** //
    gateway?(arg : MethodCallArgs, path : FSPath, callback : (error : Error, resource ?: IResource) => void)
}

Properties

parent

interface IResource
{
    parent : IResource
    // [...]
}

This property is used :

  • in the case of a MOVE, to define if the resource must be moved or just renamed.
  • in the case of any request, to define if the resource is locked by a parent resource.
  • to define if the resource is the root resource of the server.

If the resource is the root resource of the server (no parent), then the parent property must be set to null. If the resource is not the root resource of the server, then the parent property must be set to the reference to the parent resource (of type IResource). When the resource is moved to another parent, the parent property must be modified.

fsManager

interface IResource
{
    fsManager : FSManager
    // [...]
}

This property is used :

  • in the case of a serialization, to serialize a link to the fsManager for future unserialization.
  • in the case of a unserialization, to define which method to call to rebuild the serialized resource.
  • in the case of a LOCK, PUT, MKCOL involving a resource creation, to define which method to call to create a new child resource of a specific type.
  • to define if two resources are in the same file system (for instance, if two resources are managed by the same FSManager).
  • to provide to some resources a centralized management of common resources (database connection, folder to store the content, etc...).
  • to test if an object is of type IResource.

This property must not be null or undefined. If changed, this will alter the behavior of the child creation and the persistence of the resource.

Methods

create

interface IResource
{
    create(callback : SimpleCallback)
    // [...]
}

This method is called :

  • on a newly created resource (not when unserialized), in the case of PUT, COPY and LOCK involving a resource creation.

This method must be used to do some jobs when the resource is created. For instance, a PhysicalFile will create the file on the file system when the create method is called by the server. This method is called once in the life of the resource. If the server didn't created the resource, the developer must make sure to invoke it if needed. This method can be "empty" (just call the callback) if nothing is needed to be done.

delete

interface IResource
{
    delete(callback : SimpleCallback)
    // [...]
}

This method is called :

  • when the resource is irremediably removed from the server, in the case of a DELETE, MOVE (when overwritten).

This method must be used to do some jobs when the resource is deleted. This method is called once in the life of the resource. This method is NOT called when the server serialized the resources and closed. If the resource is removed by another entity than the server, the delete method must be called to ensure the jobs to be done. When successful, this method must, at least, move itself from its parent (there is a helper static method to do so : StandardResource.removeFromParent(...)).

moveTo

interface IResource
{
    moveTo(parent : IResource, newName : string, overwrite : boolean, callback : SimpleCallback)
    // [...]
}

This method is called :

  • in the case of a MOVE.

This method must handle the move of the resource into the parent resource and rename itself with the newName. The resource must be removed from the old parent and added to the new parent. The resource must be renamed (if needed). The resource must NOT overwrite/delete a conflicting resource if the overwrite parameter is equals to false. The resource must call the callback with the error Errors.ResourceAlreadyExists if an overwrite was needed but it was not allowed.

This is a lot to handle for a little method, but it can use some help from the StandardResource.standardMoveTo(...) static method. This method will move by copy the resource, involving the creation of a new resource and the deletion of the old one (and manage overwrite if needed). This behaviour is intended to fit most of situations.

rename

interface IResource
{
    rename(newName : string, callback : Return2Callback<string, string>)
    // [...]
}

This method is never used by the server itself, but it allow faster computation of moving a resource when its parent doesn't change.

The implementation of this method doesn't need to care about the conflicts. It must alter the resource name and, optionally, do some jobs (for instance, a PhysicalFile will also rename the file it is pointing to and update its real path).

write/read

interface IResource
{
    write(targetSource : boolean, callback : ReturnCallback<Writable>)
    read(targetSource : boolean, callback : ReturnCallback<Readable>)
    // [...]
}

These methods provide a stream to read or write into the file. The targetSource parameter allow to switch from the source of the content to the computed version of the content. If there is no computed version of the content or if the source of the content is not accessible (or not handled) then the targetSource parameter must be ignore and provide whatever content is possible.

mimeType

interface IResource
{
    mimeType(targetSource : boolean, callback : ReturnCallback<string>)
    // [...]
}

This method is used :

  • in the case of a PROPFIND, GET and HEAD.

It is the to provide the mime-type of the content and, if possible, its charset. The resulting string must be a valid value for the Content-Type HTTP header.

Read the write/read section to find more information about the targetSource parameter.

size

interface IResource
{
    size(targetSource : boolean, callback : ReturnCallback<number>)
    // [...]
}

This method is used :

  • in the case of a PROPFIND, GET and HEAD.

The resulting value must be the size of the content of the resource.

Read the write/read section to find more information about the targetSource parameter.

getLocks/setLock/removeLock/getLock

interface IResource
{
    getLocks(callback : ReturnCallback<Lock[]>)
    setLock(lock : Lock, callback : SimpleCallback)
    removeLock(uuid : string, callback : ReturnCallback<boolean>)
    getLock(uuid : string, callback : ReturnCallback<Lock>)
    // [...]
}

These methods allow to manage and display locks of a resource. It must affect/scan only the locks of the current resource. It must not be spread to the parent or the children.

getAvailableLocks

interface IResource
{
    getAvailableLocks(callback : ReturnCallback<LockKind[]>)
    // [...]
}

This method is used :

  • in the case of a PROPFIND.

This method allow tell which locks are supported by the resource.

Here is the current standard implementation :

getAvailableLocks(callback : ReturnCallback<LockKind[]>)
{
    callback(null, [
        new LockKind(LockScope.Exclusive, LockType.Write),
        new LockKind(LockScope.Shared, LockType.Write)
    ])
}

addChild/removeChild/getChildren

interface IResource
{
    addChild(resource : IResource, callback : SimpleCallback)
    removeChild(resource : IResource, callback : SimpleCallback)
    getChildren(callback : ReturnCallback<IResource[]>)
    // [...]
}

These methods allow to manage the children of the resource. These methods must affect the parent property if needed.

setProperty/getProperty/removeProperty/getProperties

interface IResource
{
    setProperty(name : string, value : ResourcePropertyValue, callback : SimpleCallback)
    getProperty(name : string, callback : ReturnCallback<ResourcePropertyValue>)
    removeProperty(name : string, callback : SimpleCallback)
    getProperties(callback : ReturnCallback<object>)
    // [...]
}

Allow to manage/list the properties of the resource.

Here is the ResourcePropertyValue definition :

interface XMLElement
{
    declaration ?: any
    attributes ?: any
    elements : XMLElement[]
    name ?: string
}
type ResourcePropertyValue = string | XMLElement | XMLElement[]

creationDate/lastModifiedDate

interface IResource
{
    creationDate(callback : ReturnCallback<number>)
    lastModifiedDate(callback : ReturnCallback<number>)
    // [...]
}

The resulting value of theses methods must be the milliseconds elapsed since the UNIX epoch.

You can use the Date.now() method to get this value at the current time, store it in the instance of the class and call the callbacks of these methods with the stored value.

webName

interface IResource
{
    webName(callback : ReturnCallback<string>)
    // [...]
}

This method provides the unique name of the resource among the children of its parent. This name is used for the url of the resource and, therefore, must match the HTTP url constraints.

displayName

interface IResource
{
    displayName?(callback : ReturnCallback<string>)
    // [...]
}

This method is optional. If you don't define this method, the webName method will be used instead. This method is used as content of the DAV:displayname XML element in the PROPFIND response.

There is no constraint about the text used and no unicity required. As expressed in the RFC4918, the result must describe the resource.

type

interface IResource
{
    type(callback : ReturnCallback<ResourceType>)
    // [...]
}

This method returns the type of the resource.

For more information about the type of a resource, take a look at here.

gateway

interface IResource
{
    gateway?(arg : MethodCallArgs, path : FSPath, callback : (error : Error, resource ?: IResource) => void)
    // [...]
}

This method is optional but when it is provided, it will tell the server to delegate its resource search based on the path to this method.

This way, if the server looks for the resource at /folder1/folder2/folder3/file.txt and the resource at /folder1/folder2 implements the gateway method, then the server will stop at /folder1/folder2 and ask call the resource's gateway method to provide the resource at /folder3/file.txt.

Because the arg : MethodCallArgs is provided, this method can provide a different resource tree depending on the user connected, the headers, etc...

You might find more information at the dedicated page.

Tests

You can easily test a large portion of your custom resource thanks to the ResourceTester class. Here is a little sample of the code to use :

new webdav.ResourceTester({
    canHaveVirtualFolderChildren: false,
    canHaveVirtualFileChildren: false,
    canGetLastModifiedDate: true,
    canGetCreationDate: true,
    canRemoveChildren: false,
    canHaveChildren: false,
    canGetChildren: false,
    canGetMimeType: true,
    canBeCreated: true,
    canBeDeleted: true,
    canBeRenamed: true,
    canGetSize: true,
    canBeMoved: true,
    canWrite: true,
    canRead: true,
    canLock: true
},
    // For each battery of tests, create the resource to test
    // willCreate : A value of true means you must not call the '.create(...)' method because it will be tested
    (willCreate, cb) => cb(new webdav.VirtualFile('test'))
).run((results) => {
    // Display the results of the tests
    console.log(results.all.isValid);
    if(results.all.errors)
        for(const value of results.all.errors)
            console.log(value.toString());
});

Here is an example using the willCreate :

new webdav.ResourceTester({
    canHaveVirtualFolderChildren: true,
    canHaveVirtualFileChildren: true,
    canGetLastModifiedDate: true,
    canGetCreationDate: true,
    canRemoveChildren: true,
    canHaveChildren: true,
    canGetChildren: true,
    canGetMimeType: false,
    canBeCreated: true,
    canBeDeleted: true,
    canBeRenamed: true,
    canGetSize: false,
    canBeMoved: true,
    canWrite: false,
    canRead: false
},
    (willCreate, cb) => {
        const name = path.join(rootFolder, 'testFolder' + (++fid2).toString());
        if(!willCreate)
        {
            fs.mkdir(name, () => {
                cb(new webdav.PhysicalFolder(name))
            })
        }
        else
            cb(new webdav.PhysicalFolder(name))
    }
).run((results) => {
    console.log(results.all.isValid);
    if(results.all.errors)
        for(const value of results.all.errors)
            console.log(value.toString());
})
Clone this wiki locally