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();
}
}