Ostrio:Files Utilities

These are utilities for extending the functionality of Ostrio:files.

File System Scanner:

Extends the ostrio:files object to add extra functionality:

  • Saves the homepath as a private variable to allow use of relative paths.
  • Allows serverside user to scan file system to look for files that exist but have not been added to FileCollection
  • Allows serverside user to scan file collection to check for entries that refer to files that no longer exist on the file system.
  • Allows serverside user to copy files or move physical location of files without affecting entry.
  • Allows user to specify relative path to store file when inserting into collection

FCScanner.ts

import {
    FileObj, FilesCollection, FilesCollectionConfig, FileUpload, InsertOptions
} from "meteor/ostrio:files";
import * as fs from "fs";
export class FileSystemScanner extends FilesCollection{
    private _homePath:string;

    /**
     * Same as the normal constructor, with the added requirement of a storage path.
     * @param config - The given JSON config
     * @requires config.storagePath != null
     * @requires config.storagePath does not have trailing slash.
     */
    constructor(config: FilesCollectionConfig){
        super(config);
        if(!config||!config.storagePath){
            throw 'Need specefied storage path in order to scan!';
        }
        else if(typeof config.storagePath === 'function'){
            //TODO:Check if this is alright
            this._homePath = config.storagePath(null);
        }else{
            this._homePath = config.storagePath;
        }
        console.log(this._homePath);
    }

    /***********************Scanning Methods*********************************/
    /**
     * Will run the scan on the entire tree and provide the registered files.
     * @ensure: All files within the FS's reach will be in the collection.
     * @returns: {FileObj[]} Files - A set of FileObj that were just added to the collection
     */
    fullScan():FileObj[]{
        if(Meteor.isServer){
            return this._fullScanDirectory(this._homePath,this._getRegisteredFiles(),true);
        }
    }
    /**
     *
     * @param {string} treePath - An absolute path to the subtree to scan without a trailing slash.
     * @param {boolean} recursive - Refers to whether or not the program will scan subdirectories. Defaults to true.
     * @requires treePath:string - Given path is valid and does not contain a trailing slash.
     * @returns: {FileObj[]} Files - A set of FileObj that were just added to the collection
     */
    scanFolder(treePath:string,recursive:boolean=true):FileObj[]{
        if(Meteor.isServer){
            return this._fullScanDirectory(treePath,this._getRegisteredFiles(),recursive);
        }
    }
    /**
     * A utility method that simply gets a list of the regestered files in the collection.
     * @returns {string[]} RegisteredFileList - A list of the paths to all registered files.
     * @private
     */
    private _getRegisteredFiles():string[]{
        return this.find().fetch().map((obj:FileObj)=>{return obj.path});
    }



    /**
     * Will scan a directory for files and can scan subdirectories if told to do so.
     * @NOTE: only works with files saved with absolute paths. Symbolic links may break things.
     * @param treePath - The path that will be the root of the scaned subtree.
     * @param {string[]} registeredFilePaths - A reference to a predetermed list of registered files.
     *                      Needed to minimize the number of needed queries to the collection.
     * @param {boolean} recursive - Refers to whether or not the program will all scan subdirectories. Defaults to true.
     * @private
     */
    private _fullScanDirectory(treePath:string, registeredFilePaths:string[], recursive:boolean = true):FileObj[]{
        if(Meteor.isServer){
            let files = fs.readdirSync(treePath);
            let ret:FileObj[] = [];
            //Now add files that haven't been added yet
            files.forEach((fileName:string)=>{
                //TODO: Add check for missing files. May be optional if we choose to assume no one will touch files.
                //TODO: Safer check for file?
                //TODO: Need to manually check for file and classify. Add ParseEntry function.
                //Check if the given file is an actual file rather than link or dir
                //And then check if file is already registered. If so, register file.
                let fullFilePath = treePath + '/' +fileName;
                //TODO: Add better configuration for files
                //Ignore hidden files
                if(!(fileName.indexOf('.')===0)){
                    if(_.contains(fileName,'.')&&!_.contains(registeredFilePaths,fullFilePath)){
                        super.addFile(fullFilePath,this._generateFileConfigeration(fileName),(err,fileRef)=>{if(err){throw err;}});
                        ret.push(super.findOne({path:fullFilePath}));
                    }else if(!_.contains(fileName,'.') && recursive){
                        return ret.concat(this._fullScanDirectory(fullFilePath,registeredFilePaths));
                    }
                }
            });
            return ret;
        }
    }

    /**
     * Generates a reasonable config object for the given filename.
     * @requires a valid file name with an extension of a single dot.
     * @param fileName
     * @returns addFile config
     * @private
     */
    private _generateFileConfigeration(fileName:string){
        let nameParts:string[] = fileName.split('.');
        let name = nameParts[0];
        let extension = nameParts[1].toLowerCase();
        //TODO: Investigate how type works
        //Supported Image Formats
        if(extension === 'jpg' || extension === 'png'){
            extension = 'image/'+extension;
        }else if(extension === 'mp4' || extension === 'webm' || extension === 'avi'){
            extension = 'video/'+extension;
        }else if(extension === 'pdf'){
            extension = 'pdf/'+extension;
        }
        return {
            fileName: name,
            // userId: Meteor.userId(), Find way to use userID.
            type: extension
        };
    }



    /*****************Validation Methods************************************/
    /**
     * Checks all files in the FC and removes any entries where the source file no longer exists.
     */
    validateAllFiles():void{
        if(Meteor.isServer){
            super.find().fetch().forEach((file:FileObj)=> {
                if (!FileSystemScanner.fileExists(file.path)) {
                    this.remove({path: file.path}, (err) => {
                        throw err;
                    });
                }
            });
        }
    }

    /**
     * A method that uses the accessSync method to test if a file exists since fs.existsSync requires Buffer
     * dependency and fs.exists is deprecated.
     *
     * @param filePath - Path to test
     * @returns {boolean}
     * @static
     */
    static fileExists(filePath:string):boolean{
                return Meteor.isServer && fs.existsSync(filePath);
    }
    /******************File Collection Extensions******************************/
    /**
     * The normal insert method with the added functionality of specifying a relative path to store a file.
     * To use this serverside, simply access FileObj.meta.storagePath in the storagepath method.
     * @Override
     */
    insert(settings:InsertOptions,autoStart?:boolean,relativePath:string=""): FileUpload{
        let storageField = {storagePath:relativePath};
        if(settings.meta){
            settings.meta = _.extend(settings.meta,storageField);
        }else{
            settings.meta = storageField;
        }
        return super.insert(settings, autoStart);
    }


    /******************Utility Methods******************************/
    /**
     * A method that uses the write method to copy and paste a file in the FS. Keeps original file.
     * @param sourcePath - The path to the file to be copied
     * @param destinationPath - The path to save the copy to.
     * @requires destinationPath:string contains filename of destination file.
     */
    cp(sourcePath:string, destinationPath:string){
        console.log('Doing cp checks now');
        console.log("Is Server: "+ Meteor.isServer);
        console.log("Source exists: " + FileSystemScanner.fileExists(sourcePath));
        console.log("Destination Exists: " +  FileSystemScanner.fileExists(destinationPath));
        if(Meteor.isServer && FileSystemScanner.fileExists(sourcePath) && !FileSystemScanner.fileExists(destinationPath)){
            let data = fs.readFileSync(sourcePath);
            console.log("Doing Copy Now");
            super.write(data,{fileName:destinationPath.substring(destinationPath.lastIndexOf('/')+1)},
                (err,FileRef)=>{if(err){throw err;}},false);
        }
        if(!FileSystemScanner.fileExists(sourcePath)){console.log('Source file does not exist')}
        else if(FileSystemScanner.fileExists(destinationPath)){console.log('Destination File Already Exsists');}
    }

    /**
     * Same as cp, but removes the original file.
     * @param sourcePath - The Path to the file to be copied
     * @param destinationPath - The Path to save the copy to
     * @requires destinationPath:string contains filename of destination file.
     */
    mv(sourcePath:string, destinationPath:string){
        this.cp(sourcePath, destinationPath);
        super.remove({path:sourcePath},(err)=>{if(err){throw err;}});
    }

    /**
     * Gives a server side user the ability to generate the full file path of the collection.
     * @param relativePath - The relative path to the file in questions
     * @requires relativePath:string has a prefixed slash.
     * @requires relativePath:string starts from the context of _homePath.
     * @return the full file path from the relative file.
     */
    getFullPath(relativePath:string){
        if(Meteor.isServer){
            return this._homePath + relativePath;
        }else{return '';}
    }
}

Directory Manager

A component to keep track of a file systems directories.

directory-manager.ts

import {FileObj, FilesCollection} from "meteor/ostrio:files";
import * as fs from "fs";


/**
 * A basic tree implementation that allows you to manage directories in a UNIX system with a few features.
 *
 * The tree object allows you to manage the organization of nodes
 */
export class DirectoryManager{
    private _root: DirectoryNode;
    private _homePath:string;

    /**
     * Create new file system where the root and current node are set at the given path. Also publishes and subscribes
     * to collection that makes up accessable files to user.
     *
     * @param{string} homePath - Absolute path to the current directory to make home directory.
     */
    constructor(homePath:string){
        this._homePath = homePath;
        this._root = new DirectoryNode('root', this._homePath);

    }

    /*********System Management Methods*****************/
    /**
     * Simply gives the absolute path of the homepath if the caller is the server
     * @returns {string} homepath - The absolute path to the homepath of the file system.
     */
    pwd():string{
        return (Meteor.isServer) ? this._homePath : '';
    }

    /**
     * A method that makes a new directory and builds the entire path, if neccessary
     *
     * @param {string} relativePath - Relative path with trailing slash of the new directory to create.
     * @requires this._root is a valid directory tree.
     * @requires relativePath:string !== '' to dennote root (for some reason), and there is no begining slash and
     * !relativePath.contains('.')
     *
     */
    mkdir(relativePath:string){
        let absolutePath = this._homePath + '/' + relativePath;
        if(fs.existsSync(absolutePath)){
            throw 'Folder already exists. '
        }

        let pathArray:string[] = relativePath.split('/');
        let currentNode:DirectoryNode = this._root;
        for(let i=0;i<pathArray.length;i++){
            //TODO: Make this logic less clunky
            let checkedNode = currentNode.cd(pathArray[i]);
            if(typeof checkedNode === 'undefined'){
                currentNode.addFolder(pathArray[i]);
                checkedNode = currentNode.cd(pathArray[i]);
            }
            currentNode = checkedNode;
        }
    }

    /**
     * A method where the user can provide a relative path to see if it already exists.
     * @param relativePath
     * @returns {boolean} - Whether or not the relative Path exits.
     */
    isValidPath(relativePath:string):boolean{
        let pathName:string[] = relativePath.split('/');
        let currentNode = this._root;
        for(let i=0;i<pathName.length;i++){
            currentNode = currentNode.cd(pathName[i]);
            if(typeof currentNode === 'undefined'){
                return false;
            }
        }
        return true;
    }

    /*********Utility Methods*****************/

    /**
     * Returns a primitive bash-like reporesentation of the directory system. Only lists directories right now.
     * @returns {string}
     * @override
     */
    private toString(){
        let retString:string = '';
        retString += '\u250f (root) @ ' + this.pwd();
        this._root.children.forEach(childNode => retString += childNode.toString());
        return retString;
    }

    /*********Scanning Methods *****************/
    /**
     * Scans the given node in the tree to update the file collection and all folders.
     * @NOTE: Needed here instead of node since we need the absolute path.
     * @param currentNode - The node to be scanned.
     */
    private _scanNode(currentNode:DirectoryNode):void{
        let absolutePath = this._homePath + currentNode.getRelativeStoragePath();
        let registeredFolders:string[] = currentNode.children.map((node)=>{return node.folderName});
        //Now add files that haven't been added yet
        let files = fs.readdirSync(absolutePath);
        //
        files.forEach(fileName=>{
            //TODO: Safer check for file? Maybe fs.stats?
            //Check if the given file is an actual file rather than link or dir
            //And then check if file is already registered. If so, register file.
            if(!_.contains(fileName,'.')&&!_.contains(registeredFolders,fileName)){
                registeredFolders.push(fileName);
                this._scanTree(currentNode.addFolder(fileName));
            }
        });
    }
    /**
     * Will do the same as scan node, but also scans the given subtree
     * @param rootNode - The node that will be the root of the scaned subtree.
     */
    private _scanTree(rootNode:DirectoryNode):void{
        this._scanNode(rootNode);
        rootNode.children.forEach(childDirectoryNode =>{this._scanTree(childDirectoryNode)});
    }
    /**
     * Will run the scan on the entire tree and provide the registered files and then print to screen.
     * @TODO: Check if scanTree only contains syncrounous methods.
     */
    mapSystem(){
        this._scanTree(this._root);
        console.log(this.toString());
    }
}

export class DirectoryNode{
    folderName:string;
    parent: DirectoryNode;
    systemPath: string;
    //@Override
    children:DirectoryNode[];

    /**
     * The constructor to build a new directory.
     *
     * @param name - The Name for the actual Folder
     * @param absoluteStoragePath - The path to the folder on the FS.
     * @param parent - The node that this folder is contained in. null if this is a root node.
     * @requires absoluteStoragePath:string does not include a trailing slash
     */
    constructor(name:string, absoluteStoragePath:string, parent?:DirectoryNode){
        this.folderName = name;
        this.parent = parent;
        this.children = [];
        this.systemPath = absoluteStoragePath + '/' + parent.getRelativeStoragePath() + '/' + name;
    }

    /**
     * Returns a string of this node's relative path without the trailing slash.
     * @returns {string}
     */
    getRelativeStoragePath():string{
        return (DirectoryNode.isARoot(this)) ? '' : this.parent.getRelativeStoragePath() + '/' + this.folderName;
    }

    /**
     *
     * @param newFolderName - The name of the folder as used by the tree
     * @requires {string} folderName does not contain illegal characters like '/' and absoluteStoragePath ends with newFolderName.
     */
    addFolder(newFolderName:string):DirectoryNode{
        //Check if folder exists, if it does, throw an error
        let folderNames = this.children.map((currentFolderNode:DirectoryNode)=>{return currentFolderNode.folderName});
        if(_.contains(folderNames,newFolderName)){
            throw this.folderName + " already contains a folder called " + newFolderName;
        }
        //Otherwise, make new folder
        else{
            let newNode = new DirectoryNode(newFolderName, this.systemPath + '/' + newFolderName, this);
            this.children.push();
            //@TODO: This method will default permissions to 0o0777, so we need to change that for production.
            fs.mkdir(this.systemPath + '/' +newFolderName,(err)=>{throw err});
            return newNode;
        }
    }

    /**
     * Simply removes the directory node from the tree. It is up to the user to delete the folder, since it can still contain files.
     * TODO: Consider adding feature to allow user to do this inside of this code.
     * @param toRemove - Given name of the folder to remove
     */
    removeFolder(toRemove:string){
        this.children = this.children.filter((currentNode:DirectoryNode)=>{return currentNode.folderName !== toRemove});
    }
    /**
     * Checks if parent is initialized. If not, then this node is a root.
     * @returns {boolean}
     */
    static isARoot(currentNode:DirectoryNode):boolean{
        return !currentNode.parent;
    }

    /**
     * Checks if children are initalized or if there are zero children, then assumes this is a leaf node.
     * @returns {boolean}
     */
    static isALeaf(currentNode:DirectoryNode):boolean{
        return currentNode.children.length === 0;
    }
    /**
     * Lists the current directory and its subdirectories in a primitive bash-like version.
     * @returns {string}
     * @override
     */
    toString(fileLevel:number = 0):String{
        //\u000A is newline unicode, which might (?) be more compatible
        let outputString = '\u000A' + DirectoryNode._tabLevel(fileLevel);
        if(DirectoryNode.isALeaf(this)){
            outputString += '\u2520\u2500\u2500\u2500\u2500' +this.folderName;
        }else{
            outputString += '\u2520\u2500\u2500\u2500\u252c' + this.folderName;
        }
        this.children.forEach((currentDirectoryNode,index,array)=>{
            outputString+= currentDirectoryNode.toString(fileLevel+1);
        });
        return outputString;
    }//
    static _tabLevel(fileLevel:number):string{
        let tabString = '';
        for(let i=0;i<fileLevel;i++){
            tabString += '\u2503   ';
        }
        return tabString;
    }

    /**
     * Gives a single node of the given folderName.
     * @param name
     * @requires this.children has unique values.
     * @returns {DirectoryNode}
     * @NOTE: May return 'undefined' if there are no folders with the given name.
     */
    cd(name:string):DirectoryNode{
        return _.filter(this.children,((node:DirectoryNode) => {return name===node.folderName;})).pop();
    }
}

results matching ""

    No results matching ""