├── .gitignore ├── .nvmrc ├── README.md ├── package.json ├── src ├── bootstrap-web.js ├── bootstrap.js ├── constants.js ├── consts.js ├── error_const.js ├── index-web.js ├── index.js ├── nfs-files.js ├── safe-containers.js ├── safenetwork-api.js ├── safenetwork-fs.js ├── safenetwork-utils.js └── webid_profile.js ├── webpack.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | docs/* 3 | dist/* 4 | *.log 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 8.15 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://github.com/standard/standard) 2 | 3 | # What's This? 4 | 5 | SafenetworkJs is a library for implementing web, desktop and command line apps for use with the SAFE Network. It aims to be a pretty comprehensive library. 6 | 7 | ## Supports: 8 | - Web apps running in SAFE Browser 9 | - Desktop and command line apps on Windows, Mac OS and Linux 10 | - NodeJs including cross platform packaged CLI apps (e.g. [safe-cli-boilerplate](https://github.com/theWebalyst/safe-cli-boilerplate) and [SAFE Drive](https://github.com/theWebalyst/safe-drive/)) 11 | 12 | ## Provides APIs: 13 | - authorisation with SAFE Network (via SAFE Browser) 14 | - Json API for accessing files (e.g. `listFolder(), readFile(), makeFolder(), safeFile()` etc.) 15 | - Json API for accessing public names (e.g. listing `_publicNames` their services and files) 16 | - creation of public names and services 17 | - access to SAFE network via RESTful interface in web, desktop and CLI applications via `fetch()` 18 | 19 | ## Status 20 | This NodeJs library is under development but already in use in [SAFE Drive](https://github.com/theWebalyst/safe-drive/) which mounts files, public names (DNS) and services on your local drive. SAFE Drive is a command line application which targets Windows, Mac OS and Linux. It uses SafenetworkJs for all interactions with SAFE Network including authorisation, and access to SAFE storage via a file system API, and for access to SAFE public names (DNS) and services. 21 | 22 | SafenetworkJs incorporates the code from a discontinuted web library: [safenetwork-web](https://github.com/theWebalyst/safenetwork-web) that added RESTful services for SAFE Network (implemented in the client) and was used to demonstrate a [Solid](https://solid.mit.edu/) web app runnning on SAFE Network (Safe Plume blog). Much of that code has been refactored for the safe-node-app API v0.10.x, and incorporated in SafenetworkJs. 23 | 24 | The refactoring of SAFE services is still in progress, but not a lot of work. It has been proven with the earlier SAFE NodeJs API (v0.8). A demonstration of a Solid app accessing SAFE Network using LDP implemented as a SAFE Service. This demonstrates how a web app can access SAFE Network as if it was a RESTful web server, where the RESTful API has been implemented using a SAFE Service. Presentation slides and video: [Supercharging the SAFE Network with Project Solid](https://safenetforum.org/t/devcon-talk-supercharging-the-safe-network-with-project-solid/23081?u=happybeing), (SAFE Network DevCon, April 2018, Troon Scotland). 25 | 26 | You can use SafenetworkJs APIs directly, or to emulate RESTful interfaces which access SAFE Network from web, desktop and command line applications. Extra RESTful interfaces can be supported by adding a new class. 27 | 28 | ## About SAFE Network 29 | The [SAFE Network](https://safenetwork.tech/) is a truly autonomous, decentralised internet. This **Secure Access For Everyone Network** (SAFE) tackles the increasing risks to individuals, business and nation states arising from over centralisation: domination by commercial monopolies, security risks from malware, hacking, surveillance and so on. It's a new and truly open internet aligned with the original vision held by its creators and early users, with security, net neutrality and unmediated open access baked in. 30 | 31 | The following are currently all unique to the SAFE Network (2018): 32 | 33 | - all services are secure and decentralised, including a human readable DNS 34 | - highly censorship resistant to DDoS, deep packet inspection and nation state filters 35 | - truly autonomous network 36 | - data is guaranteed to be stored and available, forever with no ongoing fees (pay once to store) 37 | - truly decentralised 'proof of resource' (farming), and not 'proof of work' or 'proof of stake' 38 | - scalable non-blockchain based storage not just of hashes of data, but the data itself 39 | - scalable non-blockchain cryptographically secured currency (Safecoin) with zero transaction fees 40 | 41 | SAFE Network operates using the resources of anonymous 'farmers' who are rewarded with Safecoin, which they can sell or use to purchase storage and other services on the network. Safecoin is efficent and scalable (non-blockchain based) secure and anonymous digital cash. 42 | 43 | SAFE is an open source project of @maidsafe, a private company which is majority owned by a Scottish charity, both based in Scotland but which is decentralised with employees and contributors based around the globe. 44 | 45 | # Development 46 | 47 | If you wish to develop with or improve this library, please see the instructions on set-up and debugging in the [SAFE Drive repository](https://github.com/theWebalyst/safe-drive/#development). 48 | 49 | Pull requests are welcome for outstanding issues and feature requests. Please note that contributions are subject to the LICENSE (see below). 50 | 51 | **IMPORTANT:** By submitting a pull request, you will be offering code under the LICENSE (below). 52 | 53 | ## Please Use Standard.js 54 | 55 | Before submitting your code please consider using `Standard.js` formatting. You may also find it helps to use an editor with support for Standard.js when developing and testing. An easy way is just to use [Atom IDE](https://atom.io/packages/atom-ide-ui) with the package [ide-standardjs] (and optionally [standard-formatter](https://atom.io/packages/standard-formatter)). Or you can install NodeJS [Standard.js](https://standardjs.com/). 56 | 57 | [![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/standard/standard) 58 | 59 | # LICENSE 60 | This project is made available under the [GPL-3.0 LICENSE](https://opensource.org/licenses/GPL-3.0) except for individual files which contain their own license so long as that file license is compatible with GPL-3.0. 61 | 62 | The responsibility for checking this licensing is valid and that your use of this code complies lies with any person and organisation making any use of it. 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "safenetworkjs", 3 | "version": "0.0.1", 4 | "description": "SAFE Network API for NodeJS and Web", 5 | "author": "theWebalyst", 6 | "license": "GPL-3.0", 7 | "main": "src/index.js", 8 | "browser": "src/index-web.js", 9 | "engines": { 10 | "node": ">=8.15.0" 11 | }, 12 | "scripts": { 13 | "test": "echo No tests yet!", 14 | "doc": "jsdoc src -r -d docs || true", 15 | "build": "webpack --mode production --verbose --colors --config webpack.config.js", 16 | "builddev": "webpack --mode development --verbose --colors --config webpack.config.js", 17 | "builddevsac": "webpack --mode development --verbose --colors --config webpack.config.js && cp -v ./dist/* ~/src/solid/solid-auth-client/src && cp -v ./dist/* ~/src/solid/solid-plume-sac/js/ && cp -v ./dist/* ~/src/solid/solid-auth-client-test/src", 18 | "dev": "webpack --watch --mode development --verbose --colors --config webpack.config.js", 19 | "buildsac": "webpack --mode production --verbose --colors --config webpack.config.js && cp -v ./dist/* ~/src/solid/solid-auth-client/src && cp -v ./dist/* ~/src/solid/solid-plume-sac/js/ && cp -v ./dist/* ~/src/solid/solid-auth-client-test/src" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/thewebalyst/safenetworkjs.git" 24 | }, 25 | "dependencies": { 26 | "@maidsafe/safe-node-app": "^0.11.1", 27 | "debug": "^4.1.1", 28 | "detect-node": "^2.0.4", 29 | "fast-text-encoding": "^1.0.0", 30 | "http-link-header": "^1.0.2", 31 | "isomorphic-fetch": "^2.2.1", 32 | "mime-types": "^2.1.22", 33 | "node-ipc": "^9.1.1", 34 | "path": "^0.12.7", 35 | "proto-fetch": "^1.0.0", 36 | "rdflib": "^0.20.1", 37 | "solid-namespace": "^0.2.0", 38 | "string": "^3.3.3" 39 | }, 40 | "bugs": { 41 | "url": "https://github.com/theWebalyst/safenetworkjs/issues" 42 | }, 43 | "homepage": "https://github.com/theWebalyst/safenetworkjs#readme", 44 | "keywords": [ 45 | "SAFEnetwork", 46 | "decentralised", 47 | "p2p", 48 | "api" 49 | ], 50 | "devDependencies": { 51 | "@babel/core": "^7.3.3", 52 | "babel": "^6.23.0", 53 | "babel-loader": "^8.0.5", 54 | "copy-webpack-plugin": "^5.0.0", 55 | "jsdoc": "^3.5.5", 56 | "webpack": "^4.29.5", 57 | "webpack-cli": "^3.2.3" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/bootstrap-web.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Authorise app and/or request access to shared Mutable Data 3 | * 4 | * This code injects methods into the nodejs SAFE API object 5 | * including one for app authorisation and one to request 6 | * access to a shared Mutable Data. 7 | */ 8 | 9 | const logApi = require('debug')('safenetworkjs:web') // Web API 10 | const Safe = window.safe 11 | 12 | /** 13 | * Detect browser environment 14 | * @return {Boolean} true if running in the browser 15 | */ 16 | Safe.isBrowser = () => { 17 | return window && (this === window) 18 | } 19 | 20 | /** 21 | * Initialise SafenetworkApi and read-only connection to SAFE Network 22 | * 23 | * Before you can use the SafenetworkApi methods, you must init and connect 24 | * with SAFE network. This function provides *read-only* init and connect, but 25 | * you can authorise subsequently using initAuthorised(), or directly with the 26 | * SAFE API. 27 | * 28 | * - if using this method you don't need to do anything with the returned SAFEAppHandle 29 | * - if authorising using another method you MUST call SafenetworkApi.setSafeAppHandle() 30 | * with a valid SAFEAppHandle 31 | * 32 | * @param {Object} appInfo information about app for auth UI 33 | * @param {function (newState)} networkStateCallback callback 34 | * @param {appOptions} appOptions [optional] SAFEApp options 35 | * @param {Object} argv [optional] required only for command line authorisation 36 | * @return {Promise} SAFEAppHandle. 37 | * 38 | * Note: see SAFE API initialiseApp() 39 | */ 40 | Safe.initUnauthorised = async (appInfo, networkStateCallback, appOptions, argv) => { 41 | // TODO review/update 42 | 43 | let result = null 44 | try { 45 | let tmpAppHandle = await Safe.initialiseApp(appInfo, networkStateCallback, appOptions) 46 | let connUri = await tmpAppHandle.auth.genConnUri() 47 | logApi('SAFEApp was initialised with a read-only session on the SafeNetwork') 48 | Safe._safeAuthUri = await Safe.authorise(connUri) 49 | logApi('SAFEApp was authorised and authUri received: ', Safe._safeAuthUri) 50 | 51 | result = await tmpAppHandle.auth.loginFromUri(Safe._safeAuthUri) 52 | } catch (err) { 53 | logApi('WARNING: ', err) 54 | } 55 | 56 | logApi('returning result: ', result) 57 | return result 58 | } 59 | 60 | /** 61 | * Initialise an authorised connection to SAFE Network 62 | * 63 | * This function provides simplified, one step authorisation. As an 64 | * alternative you can authorise separately using the SAFE API to 65 | * obtain a valid SAFEApp handle. 66 | * 67 | * @param {Object} appInfo information about your app (see SAFE API) 68 | * @param {Object} appContainers [optional] permissions to request on containers 69 | * @param {function (newState)} networkStateCallback callback 70 | * @param {Object} authOptions for app 'own_container' prop. See SAFEApp.genAuthUri() 71 | * @param {InitOptions} appOptions [optional] override default SAFEApp options 72 | * @param {Object} argv [optional] required only for command lin authorisation 73 | * @return {Promise} resolves to SAFEApp if successful 74 | */ 75 | Safe.initAuthorised = async (appInfo, appContainers, networkStateCallback, authOptions, appOptions, argv) => { 76 | logApi('Safe.initAuthorised()') 77 | let safeApp 78 | let authUri 79 | 80 | try { 81 | logApi('initialising App...') 82 | let tmpAppHandle = await Safe.initialiseApp(appInfo, networkStateCallback, appOptions) 83 | 84 | // First try init from saved auth URI 85 | safeApp = await Safe.initFromSavedUri(appInfo, networkStateCallback, appOptions, tmpAppHandle) 86 | 87 | if (!safeApp) { 88 | logApi('Authorising to obtain new authUri...') 89 | let authReqUri = await tmpAppHandle.auth.genAuthUri(appContainers, authOptions) 90 | authUri = await Safe.authorise(authReqUri) 91 | safeApp = await tmpAppHandle.auth.loginFromUri(authUri) 92 | if (safeApp) { 93 | Safe.saveAuthUri(appInfo.id, authUri) 94 | logApi('SAFEApp was authorised and authUri obtained: ', authUri) 95 | } 96 | } 97 | } catch (err) { 98 | logApi('WARNING: ', err) 99 | } 100 | 101 | Safe._safeAuthUri = authUri 102 | return safeApp 103 | } 104 | 105 | // Try using authUri from browser storage 106 | // TODO review security implications of storing authUri in browser storage 107 | 108 | /** 109 | * Attempt authoristation using URI saved in browser storage 110 | * 111 | * @param {Object} SAFE AppInfo 112 | * @param {[type]} [optional] networkStateCallback 113 | * @param {[type]} [optional] appOptions 114 | * @param {[type]} [optional] appHandle 115 | * @return {Promise} SAFEApp on success 116 | */ 117 | Safe.initFromSavedUri = async (appInfo, networkStateCallback, appOptions, appHandle) => { 118 | logApi('Safe.initFromSavedUri()') 119 | let safeApp 120 | let authUri 121 | 122 | try { 123 | logApi('initialising App...') 124 | if (!appHandle) appHandle = await Safe.initialiseApp(appInfo, networkStateCallback, appOptions) 125 | 126 | // Try using stored auth URI 127 | authUri = Safe.loadAuthUri(appInfo.id) 128 | if (authUri) { 129 | logApi('Trying stored authUri: ', authUri) 130 | safeApp = await appHandle.auth.loginFromUri(authUri) 131 | if (safeApp) { 132 | logApi('SAFEApp was authorised using stored authUri: ', authUri) 133 | } else { 134 | Safe.clearAuthUri(appInfo.id) 135 | } 136 | } 137 | } catch (err) { 138 | logApi('WARNING: ', err) 139 | } 140 | 141 | Safe._safeAuthUri = authUri 142 | return safeApp 143 | } 144 | 145 | const storageName = 'safeAuthUri' 146 | 147 | function authUriKey(appId) {return storageName + '-' + appId} 148 | 149 | Safe.saveAuthUri = (appId, authUri) => { 150 | try { 151 | window.localStorage.setItem(authUriKey(appId), authUri) 152 | } catch(e) { 153 | logApi('saveAuthUri() failed to save to browser storage:' + e.message) 154 | } 155 | } 156 | 157 | Safe.loadAuthUri = (appId) => { 158 | try { 159 | return window.localStorage.getItem(authUriKey(appId)) 160 | } catch(e) { 161 | logApi('loadAuthUri() failed to load from browser storage:' + e.message) 162 | } 163 | } 164 | 165 | Safe.clearAuthUri = (appId) => { 166 | try { 167 | window.localStorage.removeItem(authUriKey(appId)) 168 | } catch(e) { 169 | logApi('clearAuthUri() failed to clear browser storage:' + e.message) 170 | } 171 | } 172 | 173 | /** 174 | * Request permissions on a shared MD, return SAFE auth URI 175 | * 176 | * @param {SAFEApp} app 177 | * @param {String} authReqUri obtained from Safe.genShardMDataUri() 178 | * @return {Promise} authUri 179 | */ 180 | Safe.fromUri = async (app, authReqUri) => { 181 | logApi('fromUri(app, %s)', authReqUri) 182 | 183 | let safeAuthUri = await Safe.authorise(authReqUri) 184 | await app.auth.loginFromUri(safeAuthUri) 185 | return safeAuthUri 186 | } 187 | 188 | module.exports = Safe 189 | -------------------------------------------------------------------------------- /src/bootstrap.js: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License APPLICABLE TO THIS FILE ONLY which is adapted from 3 | https://github.com/project-decorum/decorum-lib/src/Safe.ts 4 | commit: 1d08f743e60c7953169290abaa37179de3508862 5 | 6 | Copyright (c) 2018 Benno Zeeman 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. */ 25 | 26 | /** 27 | * Authorise app, and/or request access to shared Mutable Data via SAFE Browser 28 | * 29 | * This code injects two methods into the nodejs SAFE API object, one for 30 | * app authorisation and one to request access to a shared Mutable Data. 31 | */ 32 | 33 | const debug = require('debug')('safenetworkjs:cli') 34 | const fs = require('fs') 35 | const ipc = require('node-ipc') 36 | const path = require('path') 37 | const Safe = require('@maidsafe/safe-node-app') 38 | 39 | /* console to file 40 | // process.__defineGetter__('stderr', function () { return fs.createWriteStream(path.join(__dirname, '/pid-' + process.pid + '-error.log'), {flags: 'a'}) }) 41 | // process.__defineGetter__('stdout', function () { return fs.createWriteStream(path.join(__dirname, '/pid-' + process.pid + '-debug'), {flags: 'a'}) }) 42 | 43 | // var fs = require('fs') 44 | var util = require('util') 45 | var logFile = fs.createWriteStream(path.join('/home/mrh/src/fuse/safenetwork-fuse/pid-' + process.pid + '-debug'), {flags: 'w'}) 46 | var logStdout = process.stdout 47 | 48 | debug = function (d) { 49 | logFile.write(util.format(d) + '\n') 50 | logStdout.write(util.format(d) + '\n') 51 | } 52 | */ 53 | 54 | // No stdout from node-ipc 55 | // ipc.config.silent = true 56 | 57 | // Request permissions on a shared MD, return SAFE auth URI 58 | Safe.fromUri = async (app, authReqUri) => { 59 | debug('fromUri(app, %s)', authReqUri) 60 | 61 | await app.auth.openUri(authReqUri) 62 | const safeAuthUri = await ipcReceive(String(process.pid)) 63 | return app.auth.loginFromUri(safeAuthUri) 64 | } 65 | 66 | // Request unauthorised connection (read-only access to network) 67 | Safe.initUnauthorised = async (appInfo = untrustedAppInfo, networkStateCallback, appOptions, argv) => { 68 | const connectAuthorised = false 69 | return Safe._init(appInfo, undefined, 70 | networkStateCallback, undefined, appOptions, argv, connectAuthorised) 71 | } 72 | 73 | // Request authorisation 74 | Safe.initAuthorised = async (appInfo, appContainers, 75 | networkStateCallback, authOptions, appOptions, argv) => { 76 | const connectAuthorised = true 77 | return Safe._init(appInfo, appContainers, 78 | networkStateCallback, authOptions, appOptions, argv, connectAuthorised) 79 | } 80 | 81 | Safe._init = async (appInfo, appContainers, networkStateCallback, authOptions, appOptions, argv) => { 82 | debug('__dirname: ' + String(__dirname)) 83 | debug('\nSafe.initAuthorised()\n with appInfo: ' + JSON.stringify(appInfo) + 84 | ' argv: ' + JSON.stringify(argv)) 85 | 86 | const options = { 87 | libPath: getLibPath() 88 | } 89 | 90 | if (argv.pid !== undefined) { 91 | if (argv.uri === undefined) { 92 | throw Error('--uri undefined') 93 | } 94 | 95 | debug('ipcSend(' + argv.pid + ',' + argv.uri + ')') 96 | await ipcSend(String(argv.pid), argv.uri) 97 | 98 | process.exit() 99 | } 100 | 101 | let uri 102 | if (argv.uri !== undefined) { 103 | uri = argv.uri 104 | } else { 105 | await authorise(process.pid, appInfo, appContainers, networkStateCallback, authOptions, appOptions) 106 | debug('ipcReceive(' + process.pid + ')') 107 | uri = await ipcReceive(String(process.pid)) 108 | } 109 | 110 | return Safe.fromAuthUri(appInfo, uri, null, appOptions) 111 | } 112 | 113 | async function authorise (pid, appInfo, appContainers, networkStateCallback, authOptions, appOptions, connectAuthorised) { 114 | connectAuthorised = (connectAuthorised === undefined ? true : false) 115 | 116 | // For development can provide a pre-compiled cmd to receive the auth URL 117 | // This allows the application to be run and debugged using node 118 | if (!appInfo.customExecPath) { 119 | appInfo.customExecPath = [ 120 | process.argv[0], process.argv[1], 121 | '--pid', String(pid), 122 | '--uri' 123 | ] 124 | } 125 | debug('call Safe.initialiseApp() with \nappInfo: ' + JSON.stringify(appInfo) + 126 | '\noptions: ' + JSON.stringify(appOptions)) 127 | 128 | const app = await Safe.initialiseApp(appInfo, networkStateCallback, appOptions) 129 | debug('call app.auth.genAuthUri() with appContainers: \n' + JSON.stringify(appContainers) + 130 | '\nappOptions: \n' + JSON.stringify(authOptions)) 131 | 132 | let uri 133 | if (connectAuthorised) { 134 | uri = await app.auth.genAuthUri(appContainers, authOptions) 135 | } else { 136 | uri = await app.auth.getConnUri() 137 | } 138 | 139 | debug('call app.auth.openUri() with uri: \n' + JSON.stringify(uri.uri)) 140 | await app.auth.openUri(uri.uri) 141 | debug('wait a mo') 142 | } 143 | 144 | async function ipcReceive (id) { 145 | debug('ipcReceive(' + id + ')') 146 | return new Promise((resolve) => { 147 | ipc.config.id = id 148 | 149 | ipc.serve(() => { 150 | ipc.server.on('auth-uri', (data) => { 151 | debug('on(auth-uri) handling data.message: ' + data.message) 152 | resolve(data.message) 153 | ipc.server.stop() 154 | }) 155 | }) 156 | 157 | ipc.server.start() 158 | }) 159 | } 160 | 161 | async function ipcSend (id, data) { 162 | debug('ipcSend(' + id + ', ' + data + ')') 163 | 164 | return new Promise((resolve) => { 165 | ipc.config.id = id + '-cli' 166 | 167 | ipc.connectTo(id, () => { 168 | ipc.of[id].on('connect', () => { 169 | debug('on(connect)') 170 | ipc.of[id].emit('auth-uri', { id: ipc.config.id, message: data }) 171 | 172 | resolve() 173 | ipc.disconnect('world') 174 | }) 175 | }) 176 | }) 177 | } 178 | 179 | /** 180 | * @returns 181 | */ 182 | function getLibPath () { 183 | const roots = [ 184 | path.dirname(process.argv[0]), 185 | path.dirname(process.argv[1]) 186 | ] 187 | 188 | const locations = [ 189 | 'node_modules/@maidsafe/safe-node-app/src/native' 190 | ] 191 | 192 | for (const root of roots) { 193 | for (const location of locations) { 194 | const dir = path.join(root, location) 195 | 196 | if (fs.existsSync(dir)) { 197 | debug('getLibPath() returning: ', dir) 198 | return dir 199 | } 200 | } 201 | } 202 | 203 | debug('No library directory found.') 204 | throw Error('No library directory found.') 205 | } 206 | 207 | module.exports = Safe 208 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SafenetworkJs constants 3 | * 4 | * Also includes SAFE constants not exposed by safe_app_nodejs.CONSTANTS 5 | * Ref: https://github.com/maidsafe/safe_app_nodejs/blob/83a5bc47575270723c2fa748cde104d0ec770250/src/consts.js#L15 6 | * 7 | * TODO replace codes here that are now present above 8 | */ 9 | 10 | const CONSTANTS = { 11 | // SafenetworkJs: 12 | SUCCESS: null, 13 | 14 | /* 15 | * What constitutes a valid public name is not specified in the containers 16 | * RFC, but the Web Hosting Manager example code specifies: 17 | * "Public ID must contain only lowercase alphanumeric characters. 18 | * Should container a min of 3 characters and a max of 62 characters." 19 | * 20 | * Refs: 21 | * https://github.com/maidsafe/rfcs/blob/master/text/0046-new-auth-flow/containers.md 22 | */ 23 | // TODO ideally these would be SAFE API constants: 24 | PUBLICNAME_MINCHARS: 3, 25 | PUBLICNAME_MAXCHARS: 62, 26 | BADPUBLICNAME_MSG: 'must contain only lowercase alphanumeric characters. Should container a min of 3 characters and a max of 62 characters.', 27 | 28 | MD_METADATA_KEY: '_metadata', 29 | 30 | DATA_TYPE_MD: 'MD', 31 | DATA_TYPE_IMMD: 'IMMD', 32 | DATA_TYPE_NFS: 'NFS', 33 | DATA_TYPE_RDF: 'RDF', 34 | 35 | ENV: { 36 | DEV: 'development', 37 | TEST: 'test', 38 | PROD: 'production' 39 | }, 40 | TYPE_TAG: { 41 | DNS: 15001, 42 | WWW: 15002, 43 | NFS: 15002, 44 | WEBID: 16048 // From maidsafe/safe-web-id-manager-js-fork/src/actions/webIds_actions.js 45 | }, 46 | ERROR_CODE: { 47 | ENCODE_DECODE_ERROR: -1, 48 | SYMMETRIC_DECIPHER_FAILURE: -3, 49 | ACCESS_DENIED: -100, 50 | DATA_EXISTS: -104, 51 | NO_SUCH_ENTRY: -106, 52 | ENTRY_EXISTS: -107, 53 | TOO_MANY_ENTRIES: -108, 54 | NO_SUCH_KEY: -109, 55 | LOW_BALANCE: -113, 56 | NFS_FILE_NOT_FOUND: -301, 57 | INVALID_SIGN_KEY_HANDLE: -1011, 58 | EMPTY_DIR: -1029, 59 | 60 | // SafenetworkJS errors: 61 | UNKNOWN_ERROR: -2000, 62 | INVALID_FILE_DESCRIPTOR: -2001 63 | }, 64 | APP_ERR_CODE: { 65 | INVALID_PUBLIC_NAME: -10001, 66 | INVALID_AUTH_RESP: -10002, 67 | INVALID_SHARED_MD_RESP: -10003, 68 | APP_NOT_INITIALISED: -10004, 69 | INVALID_SERVICE_PATH: -10005, 70 | INVALID_SERVICE_META: -10006, 71 | INVALID_SERVICE_NAME: -10007, 72 | ENTRY_VALUE_NOT_EMPTY: -10008 73 | }, 74 | MAX_FILE_SIZE: 20 * 1024 * 1024, 75 | NETWORK_STATE: { 76 | INIT: 'Init', 77 | CONNECTED: 'Connected', 78 | UNKNOWN: 'Unknown', 79 | DISCONNECTED: 'Disconnected' 80 | }, 81 | // FILE_OPEN_MODE: { 82 | // OPEN_MODE_READ: 4 83 | // }, 84 | FILE_READ: { 85 | FROM_START: 0, 86 | TILL_END: 0 87 | }, 88 | SERVICE_TYPE_POSTFIX_DELIM: '@', 89 | DOWNLOAD_CHUNK_SIZE: 1000000, 90 | UPLOAD_CHUNK_SIZE: 1000000, 91 | UI: { 92 | DEFAULT_SERVICE_CONTAINER_PREFIX: 'root-', 93 | MSG: { 94 | CREATING_PUBLIC_NAMES: 'Creating public name', 95 | FETCH_SERVICE_CONTAINERS: 'Fetching service containers', 96 | CHECK_PUB_ACCESS: 'Checking public name access', 97 | CHECK_SERVICE_EXISTS: 'Checking service exists', 98 | SERVICE_EXISTS: 'Service already exists', 99 | DELETING_SERVICE: 'Deleting service', 100 | FETCHING_SERVICE: 'Fetching service', 101 | MD_AUTH_WAITING: 'Waiting for Mutable Data authorisation', 102 | GETTING_CONT_INFO: 'Getting container information', 103 | PUBLISHING_WEB: 'Publishing website', 104 | DELETING_FILES: 'Deleting file or folder', 105 | DOWNLOADING_FILE: 'Downloading file', 106 | REMAPPING_SERVICE: 'Remapping service', 107 | UPLOADING_TEMPLATE: 'Uploading template' 108 | }, 109 | ERROR_MSG: { 110 | LOW_BALANCE: 'Network operation is not possible as there is insufficient account balance', 111 | NO_SUCH_ENTRY: 'Data not found', 112 | ENTRY_EXISTS: 'Data already exists', 113 | NO_SUCH_KEY: 'Unable to fetch data', 114 | INVALID_PUBLIC_NAME: 'Public ID must contain only lowercase alphanumeric characters. Should contain a min of 3 characters and a max of 62 characters', 115 | INVALID_SERVICE_NAME: 'Service name must contain only lowercase alphanumeric characters. Should contain a min of 3 characters and a max of 62 characters' 116 | } 117 | } 118 | } 119 | 120 | module.exports = CONSTANTS 121 | -------------------------------------------------------------------------------- /src/consts.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 MaidSafe.net limited. 2 | // 3 | // This SAFE Network Software is licensed to you under 4 | // the MIT license or 5 | // the Modified BSD license , 6 | // at your option. 7 | // 8 | // This file may not be copied, modified, or distributed except according to those terms. 9 | // 10 | // Please review the Licences for the specific language governing permissions and limitations 11 | // relating to use of the SAFE Network Software. 12 | 13 | const os = require('os'); 14 | 15 | const TAG_TYPE_DNS = 15001; 16 | const TAG_TYPE_WWW = 15002; 17 | 18 | const NET_STATE_INIT = -100; 19 | const NET_STATE_DISCONNECTED = -1; 20 | const NET_STATE_CONNECTED = 0; 21 | 22 | const LIB_LOCATION_MOCK = 'mock'; 23 | const LIB_LOCATION_PROD = 'prod'; 24 | 25 | /** 26 | * @param {Number} NFS_FILE_MODE_OVERWRITE NFS File open in overwrite mode. 27 | * When used as the `openMode` parameter for `nfs.open(, )` the entire content 28 | * of the file will be replaced when writing data to it. 29 | * 30 | * @param {Number} NFS_FILE_MODE_APPEND NFS File open in append mode. 31 | * When used as the `openMode` param for `nfs.open(, )` any new content 32 | * written to the file will be appended to the end without modifying existing data. 33 | * 34 | * @param {Number} NFS_FILE_MODE_READ NFS File open in read-only mode. 35 | * When used as the `openMode` param for `nfs.open(, )` only the read 36 | * operation is allowed. 37 | * 38 | * @param {Number} NFS_FILE_START Read the file from the beginning. 39 | * When used as the `position` param for the NFS `file.read(, )` 40 | * function, the file will be read from the beginning. 41 | * 42 | * @param {Number} NFS_FILE_END Read until the end of a file. 43 | * When used as the `length` param for the NFS `file.read(, )` 44 | * function, the file will be read from the position provided until the end 45 | * of its content. E.g. if `NFS_FILE_START` and `NFS_FILE_END` are passed in as 46 | * the `position` and `length` parameters respectively, then the whole content of the 47 | * file will be read. 48 | * 49 | * @param {Number} USER_ANYONE Any user. 50 | * When used as the `signkey` param in any of the MutableData functions to 51 | * manipulate user permissions, like `getUserPermissions`, `setUserPermissions`, 52 | * `delUserPermissions`, etc., this will associate the permissions operation to 53 | * any user rather than to a particular sign key. 54 | * E.g. if this constant is used as the `signkey` param of 55 | * the `setUserPermissions(, , )` function, 56 | * the permissions in the `permissionSet` provided will be granted to anyone 57 | * rather to a specific user's/aplication's sign key. 58 | * 59 | * @param {String} MD_METADATA_KEY MutableData's entry key where its metadata is stored. 60 | * The MutableData's metadata can be set either when invoking the `quickSetup` 61 | * function or by invking the `setMetadata` function. 62 | * The metadata is stored as an encoded entry in the MutableData which key 63 | * is `MD_METADATA_KEY`, thus this constant can be used to realise which of the 64 | * entries is not application's data but the MutableData's metadata instead. 65 | * The metadata is particularly used by the Authenticator when another 66 | * application has requested mutation permissions on a MutableData, 67 | * displaying this information to the user, so the user can make a better 68 | * decision to either allow or deny such a request based on it. 69 | * 70 | * @param {Number} MD_ENTRIES_EMPTY Represents an empty set of MutableData's entries. 71 | * This can be used when invoking the `put` function of the MutableData API to 72 | * signal that it should be committed to the network with an empty set of entries. 73 | * 74 | * @param {Number} MD_PERMISSION_EMPTY Represents an empty set of MutableData's permissions. 75 | * This can be used when invoking the `put` function of the MutableData API to 76 | * signal that it should be committed to the network with an empty set of permissions. 77 | * 78 | * @param {Number} GET_NEXT_VERSION Gets next correct file version. 79 | * This constant may be used in place of the version argument when 80 | * invoking `update` function of the NFS API to automatically obtain correct file version. 81 | * 82 | */ 83 | 84 | 85 | /**/ 86 | const pubConsts = { 87 | NFS_FILE_MODE_OVERWRITE: 1, 88 | NFS_FILE_MODE_APPEND: 2, 89 | NFS_FILE_MODE_READ: 4, 90 | NFS_FILE_START: 0, 91 | NFS_FILE_END: 0, 92 | USER_ANYONE: 0, 93 | MD_METADATA_KEY: '_metadata', 94 | MD_ENTRIES_EMPTY: 0, 95 | MD_PERMISSION_EMPTY: 0, 96 | GET_NEXT_VERSION: 0 97 | }; 98 | 99 | const SAFE_APP_LIB_FILENAME = { 100 | win32: 'safe_app.dll', 101 | darwin: 'libsafe_app.dylib', 102 | linux: 'libsafe_app.so' 103 | }[os.platform()]; 104 | 105 | const SYSTEM_URI_LIB_FILENAME = { 106 | win32: 'system_uri.dll', 107 | darwin: 'libsystem_uri.dylib', 108 | linux: 'libsystem_uri.so' 109 | }[os.platform()]; 110 | 111 | const INDEX_HTML = 'index.html'; 112 | 113 | const CID_VERSION = 1; 114 | const CID_BASE_ENCODING = 'base32z'; 115 | const CID_HASH_FN = 'sha3-256'; 116 | const CID_DEFAULT_CODEC = 'raw'; 117 | const CID_MIME_CODEC_PREFIX = 'mime/'; 118 | 119 | module.exports = { 120 | TAG_TYPE_DNS, 121 | TAG_TYPE_WWW, 122 | 123 | NET_STATE_INIT, 124 | NET_STATE_DISCONNECTED, 125 | NET_STATE_CONNECTED, 126 | 127 | LIB_LOCATION_MOCK, 128 | LIB_LOCATION_PROD, 129 | 130 | INDEX_HTML, 131 | pubConsts, 132 | 133 | SAFE_APP_LIB_FILENAME, 134 | SYSTEM_URI_LIB_FILENAME, 135 | 136 | CID_VERSION, 137 | CID_BASE_ENCODING, 138 | CID_HASH_FN, 139 | CID_DEFAULT_CODEC, 140 | CID_MIME_CODEC_PREFIX, 141 | }; 142 | -------------------------------------------------------------------------------- /src/error_const.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 MaidSafe.net limited. 2 | // 3 | // This SAFE Network Software is licensed to you under 4 | // the MIT license or 5 | // the Modified BSD license , 6 | // at your option. 7 | // 8 | // This file may not be copied, modified, or distributed except according to those terms. 9 | // 10 | // Please review the Licences for the specific language governing permissions and limitations 11 | // relating to use of the SAFE Network Software. 12 | 13 | // This file copied from Web Hosting Manager for use with code 14 | // included in webid_profile.js 15 | // TODO rationalise when error codes are part of SAFE API constants 16 | 17 | module.exports = { 18 | 19 | /** 20 | * @typedef ERR_SERIALISING_DESERIALISING 21 | * @type {object} 22 | * @description Thrown natively when failing to encrypt/decrypt a MD entry 23 | * @property {number} code -1 24 | * @property {string} msg 25 | */ 26 | ERR_SERIALISING_DESERIALISING: { 27 | code: -1, 28 | msg: 'Error while serialising/deserialising.' 29 | }, 30 | 31 | /** 32 | * @typedef ERR_NO_SUCH_DATA 33 | * @type {object} 34 | * @description Thrown natively when data not found on network. 35 | * @property {number} code -103 36 | * @property {string} msg 37 | */ 38 | ERR_NO_SUCH_DATA: { 39 | code: -103, 40 | msg: 'No such data.' 41 | }, 42 | 43 | /** 44 | * @typedef ERR_DATA_GIVEN_ALREADY_EXISTS 45 | * @type {object} 46 | * @description Thrown natively when data already exists at the target address on network. 47 | * @property {number} code -104 48 | * @property {string} msg 49 | */ 50 | ERR_DATA_GIVEN_ALREADY_EXISTS: { 51 | code: -104, 52 | msg: 'Data already exists at the target address.' 53 | }, 54 | 55 | /** 56 | * @typedef ERR_NO_SUCH_ENTRY 57 | * @type {object} 58 | * @description Thrown natively when entry on found in MutableData. 59 | * @property {number} code -106 60 | * @property {string} msg 61 | */ 62 | ERR_NO_SUCH_ENTRY: { 63 | code: -106, 64 | msg: 'Entry does not exist.' 65 | }, 66 | 67 | /** 68 | * @typedef ERR_FILE_NOT_FOUND 69 | * @type {object} 70 | * @description Thrown natively when NFS-style file not found. 71 | * @property {number} code -301 72 | * @property {string} msg 73 | */ 74 | ERR_FILE_NOT_FOUND: { 75 | code: -301, 76 | msg: 'File not found.' 77 | }, 78 | 79 | /** 80 | * @typedef INVALID_BYTE_RANGE 81 | * @type {object} 82 | * @description Thrown natively when attempting to fetch partial 83 | * byte range of NFS-style file that is not within the total byte range. 84 | * For example, this error is thrown if a file is 10 bytes long, 85 | * however a byte range of 20 is requested. 86 | * @property {number} code -302 87 | * @property {string} msg 88 | */ 89 | INVALID_BYTE_RANGE: { 90 | code: -302, 91 | msg: 'NFS error: Invalid byte range specified' 92 | }, 93 | 94 | /** 95 | * @typedef FAILED_TO_LOAD_LIB 96 | * @type {object} 97 | * @description Thrown when a native library fails to load and which library. 98 | * @property {number} code 1000 99 | * @property {function} msg 100 | */ 101 | FAILED_TO_LOAD_LIB: { 102 | code: 1000, 103 | msg: (e) => `Failed to load native libraries: ${e}` 104 | }, 105 | 106 | /** 107 | * @typedef SETUP_INCOMPLETE 108 | * @type {object} 109 | * @description Informs that app is not yet connected to network. 110 | * @property {number} code 1001 111 | * @property {string} msg 112 | */ 113 | SETUP_INCOMPLETE: { 114 | code: 1001, 115 | msg: 'Setup Incomplete. Connection not available yet.' 116 | }, 117 | 118 | /** 119 | * @typedef MALFORMED_APP_INFO 120 | * @type {object} 121 | * @description Informs when {@link AppInfo} provided during initialisation is invalid. 122 | * @property {number} code 1002 123 | * @property {string} msg 124 | */ 125 | MALFORMED_APP_INFO: { 126 | code: 1002, 127 | msg: ` 128 | Malformed appInfo. 129 | Please conform to proper format and be sure "id", "name", and "vendor" properties are defined: 130 | { 131 | id: 'net.maidsafe.example.id', 132 | name: 'Name of App', 133 | vendor: 'MaidSafe Ltd.', 134 | scope: null 135 | }` 136 | }, 137 | 138 | /** 139 | * @typedef MISSING_PERMS_ARRAY 140 | * @type {object} 141 | * @description Argument should be an array object. 142 | * @property {number} code 1003 143 | * @property {string} msg 144 | */ 145 | MISSING_PERMS_ARRAY: { 146 | code: 1003, 147 | msg: 'Argument should be an array object' 148 | }, 149 | 150 | /** 151 | * @typedef INVALID_SHARE_MD_PERMISSION 152 | * @type {object} 153 | * @description Informs of a specific object in a share MData permissions array that is malformed. 154 | * @property {number} code 1004 155 | * @property {function} msg 156 | */ 157 | INVALID_SHARE_MD_PERMISSION: { 158 | code: 1004, 159 | msg: (perm) => `Invalid share MData permission: ${perm}` 160 | }, 161 | 162 | /** 163 | * @typedef INVALID_PERMS_ARRAY 164 | * @type {object} 165 | * @description Thrown when share MD permissions is not an array. 166 | * @property {number} code 1005 167 | * @property {string} msg 168 | */ 169 | INVALID_PERMS_ARRAY: { 170 | code: 1005, 171 | msg: 'Permissions provided are not in array format' 172 | }, 173 | 174 | /** 175 | * @typedef MISSING_URL 176 | * @type {object} 177 | * @description Please provide URL 178 | * @property {number} code 1006 179 | * @property {string} msg 180 | */ 181 | MISSING_URL: { 182 | code: 1006, 183 | msg: 'Please provide URL' 184 | }, 185 | 186 | /** 187 | * @typedef INVALID_URL 188 | * @type {object} 189 | * @description Please provide URL in string format. 190 | * @property {number} code 1007 191 | * @property {string} msg 192 | */ 193 | INVALID_URL: { 194 | code: 1007, 195 | msg: 'Please provide URL in string format' 196 | }, 197 | 198 | /** 199 | * @typedef MISSING_AUTH_URI 200 | * @type {object} 201 | * @description Thrown when attempting to connect without authorisation URI. 202 | * @property {number} code 1008 203 | * @property {string} msg 204 | */ 205 | MISSING_AUTH_URI: { 206 | code: 1008, 207 | msg: 'Please provide auth URI' 208 | }, 209 | 210 | /** 211 | * @typedef NON_AUTH_GRANTED_URI 212 | * @type {object} 213 | * @description Thrown when attempting extract granted access permissions 214 | * from a URI which doesn't contain such information. 215 | * @property {number} code 1009 216 | * @property {string} msg 217 | */ 218 | NON_AUTH_GRANTED_URI: { 219 | code: 1009, 220 | msg: 'The URI provided is not for an authenticated app with permissions information' 221 | }, 222 | 223 | /** 224 | * @typedef INVALID_PERM 225 | * @type {object} 226 | * @description Thrown when invalid permission is requested on container. 227 | * @property {number} code 1010 228 | * @property {function} msg 229 | */ 230 | INVALID_PERM: { 231 | code: 1010, 232 | msg: (perm) => `${perm} is not a valid permission` 233 | }, 234 | 235 | /** 236 | * @typedef MISSING_CONTAINER_STRING 237 | * @type {object} 238 | * @description Thrown when attempting to get a container without specifying name with a string. 239 | * @property {number} code 1011 240 | * @property {string} msg 241 | */ 242 | MISSING_CONTAINER_STRING: { 243 | code: 1011, 244 | msg: 'Please provide container string argument' 245 | }, 246 | 247 | /** 248 | * @typedef NON_DEV 249 | * @type {object} 250 | * @description Thrown when functions unique to testing environment are attempted to be used. 251 | * @property {number} code 1012 252 | * @property {string} msg 253 | */ 254 | NON_DEV: { 255 | code: 1012, 256 | msg: ` 257 | Not supported outside of Testing Environment. 258 | Set NODE_ENV=test` 259 | }, 260 | 261 | /** 262 | * @typedef MISSING_PUB_ENC_KEY 263 | * @type {object} 264 | * @description Thrown when public encryption key is not provided as necessary function argument. 265 | * @property {number} code 1013 266 | * @property {string} msg 267 | */ 268 | MISSING_PUB_ENC_KEY: { 269 | code: 1013, 270 | msg: ` 271 | Please provide public encryption key. 272 | For example: 273 | - app.crypto.getAppPubEncKey() 274 | - const encKeyPair = app.crypto.generateEncKeyPair(); 275 | encKeyPair.pubEncKey;` 276 | }, 277 | 278 | /** 279 | * @typedef MISSING_SEC_ENC_KEY 280 | * @type {object} 281 | * @description Thrown when secret encryption key is not provided as necessary function argument. 282 | * @property {number} code 1014 283 | * @property {function} msg 284 | */ 285 | MISSING_SEC_ENC_KEY: { 286 | code: 1014, 287 | msg: (size) => ` 288 | Please provide ${size} byte secret encryption key: 289 | const encKeyPair = app.crypto.generateEncKeyPair(); 290 | encKeyPair.secEncKey;` 291 | }, 292 | 293 | /** 294 | * @typedef LOGGER_INIT_ERROR 295 | * @type {object} 296 | * @description Logger initialisation failed. 297 | * @property {number} code 1015 298 | * @property {function} msg 299 | */ 300 | LOGGER_INIT_ERROR: { 301 | code: 1015, 302 | msg: (e) => `Logger initialisation failed. Reason: ${e}` 303 | }, 304 | 305 | /** 306 | * @typedef CONFIG_PATH_ERROR 307 | * @type {object} 308 | * @description Informs you when config search path has failed to set, with specific reason. 309 | * @property {number} code 1016 310 | * @property {function} msg 311 | */ 312 | CONFIG_PATH_ERROR: { 313 | code: 1016, 314 | msg: (e) => `Failed to set additional config search path. Reason: ${e}` 315 | }, 316 | 317 | /** 318 | * @typedef XOR_NAME 319 | * @type {object} 320 | * @description Custom name used to create public or private 321 | * MutableData must be 32 bytes in length. 322 | * @property {number} code 1017 323 | * @property {function} msg 324 | */ 325 | XOR_NAME: { 326 | code: 1017, 327 | msg: (size) => `Name _must be_ provided and ${size} bytes long.` 328 | }, 329 | 330 | /** 331 | * @typedef NONCE 332 | * @type {object} 333 | * @description Any string or buffer provided to private MutableData 334 | * that is not 24 bytes in length will throw error. 335 | * @property {number} code 1018 336 | * @property {function} msg 337 | */ 338 | NONCE: { 339 | code: 1018, 340 | msg: (size) => `Nonce _must be_ provided and ${size} bytes long.` 341 | }, 342 | 343 | /** 344 | * @typedef TYPE_TAG_NAN 345 | * @type {object} 346 | * @description Tag argument when creating private or public MutableData must be a number. 347 | * @property {number} code 1019 348 | * @property {string} msg 349 | */ 350 | TYPE_TAG_NAN: { 351 | code: 1019, 352 | msg: 'Type tag provided _must be_ an integer' 353 | }, 354 | 355 | /** 356 | * @typedef INVALID_SEC_KEY 357 | * @type {object} 358 | * @description Secret encryption key of improper length is provided to custom private MutableData 359 | * @property {number} code 1020 360 | * @property {function} msg 361 | */ 362 | INVALID_SEC_KEY: { 363 | code: 1020, 364 | msg: (size) => `Secret encryption key _must be_ provided and ${size} bytes long.` 365 | }, 366 | 367 | /** 368 | * @typedef EXPERIMENTAL_API_DISABLED 369 | * @type {object} 370 | * @description Thrown when functions that are experimental APIs were 371 | * not enabled but attempted to be used 372 | * @property {number} code 1021 373 | * @property {string} msg 374 | */ 375 | EXPERIMENTAL_API_DISABLED: { 376 | code: 1021, 377 | msg: (fn) => ` 378 | The '${fn}' is disabled as it's part of the set of experimental APIs. 379 | Pass --enable-experimental-apis argument to the application, or programatically 380 | set the 'enableExperimentalApis' flag in the initialisation options to enable them.` 381 | }, 382 | 383 | /** 384 | * @typedef ERR_SERVICE_NOT_FOUND 385 | * @type {Object} 386 | * @description the service/subname was not found 387 | * @property {number} code 1022 388 | * @property {function} msg 389 | */ 390 | ERR_SERVICE_NOT_FOUND: { 391 | code: 1022, 392 | msg: 'Requested service is not found.' 393 | }, 394 | 395 | /** 396 | * @typedef ERR_CONTENT_NOT_FOUND 397 | * @type {Object} 398 | * @description the content was not found at the address provided 399 | * @property {number} code 1023 400 | * @property {function} msg 401 | */ 402 | ERR_CONTENT_NOT_FOUND: { 403 | code: 1023, 404 | msg: 'No content found at requested address.' 405 | }, 406 | 407 | /** 408 | * @typedef INVALID_RDF_LOCATION 409 | * @type {Object} 410 | * @description RDF Location provided is not and object with name/typeTag 411 | * @property {number} code 1024 412 | * @property {function} msg 413 | */ 414 | INVALID_RDF_LOCATION: { 415 | code: 1024, 416 | msg: 'RDF Location _must_ be an object of the form { name, typeTag }.' 417 | }, 418 | 419 | /** 420 | * @typedef INVALID_PUBNAME 421 | * @type {Object} 422 | * @description public name provided is not valid 423 | * @property {number} code 1025 424 | * @property {function} msg 425 | */ 426 | INVALID_PUBNAME: { 427 | code: 1025, 428 | msg: 'A publicName string _must_ be passed for adding services to a publicName.' 429 | }, 430 | 431 | /** 432 | * @typedef INVALID_SUBNAME 433 | * @type {Object} 434 | * @description RDF Location provided is not and object with name/typeTag 435 | * @property {number} code 1026 436 | * @property {function} msg 437 | */ 438 | INVALID_SUBNAME: { 439 | code: 1026, 440 | msg: 'A subName string _must_ be passed for adding services to a publicName.' 441 | }, 442 | 443 | /** 444 | * @typedef MISSING_RDF_ID 445 | * @type {Object} 446 | * @description RDF object does not have an ID. 447 | * @property {number} code 1027 448 | * @property {function} msg 449 | */ 450 | MISSING_RDF_ID: { 451 | code: 1027, 452 | msg: 'No ID has been found in the RDF graph.' 453 | }, 454 | }; 455 | -------------------------------------------------------------------------------- /src/index-web.js: -------------------------------------------------------------------------------- 1 | // SAFE Client Libs API is attached to SAFE Browser window 2 | if (typeof window === 'undefined') { 3 | let errMsg = 'ERROR: window is not defined but are you are using the browser build of SafeentworkJS' 4 | console.log(errMsg) 5 | throw new Error(errMsg) 6 | } 7 | 8 | if (typeof window.safe === 'undefined') { 9 | let errMsg = 'WARNING: window.safe (SAFE Network API) is not defined, are you running in SAFE Browser?' 10 | console.log(errMsg) 11 | throw new Error(errMsg) 12 | } 13 | 14 | // SafenetworkJs libraries 15 | const Safenetworkjs = require('./safenetwork-api') 16 | 17 | // SafenetworkApi instance with SAFE Client Libs API 18 | const safeApi = require('./bootstrap-web') 19 | const safeJs = new Safenetworkjs.SafenetworkApi(safeApi) 20 | window.safeJs = safeJs 21 | 22 | /* 23 | * Override window.fetch() in order to support safe:// URIs 24 | */ 25 | 26 | // Protocol handlers for fetch() 27 | const httpFetch = require('isomorphic-fetch') 28 | const protoFetch = require('proto-fetch') 29 | 30 | // map protocols to fetch() 31 | const fetch = protoFetch({ 32 | http: httpFetch, 33 | https: httpFetch, 34 | safe: safeJs.fetch.bind(safeJs) 35 | // https: Safenetwork.fetch.bind(Safenetwork), // Debugging with SAFE mock browser 36 | }) 37 | 38 | // SafenetworkApi class 39 | exports = module.exports = Safenetworkjs.SafenetworkApi 40 | module.exports.SafenetworkApi = Safenetworkjs.SafenetworkApi 41 | 42 | module.exports.safeJs = safeJs 43 | module.exports.protoFetch = protoFetch 44 | module.exports.WebIdProfile = require('./webid_profile') 45 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // Check we're not running in a browser 2 | if (typeof window !== 'undefined') { 3 | let errMsg = 'ERROR: Web apps must use Safenetworkjs browser build' 4 | console.log(errMsg) 5 | throw new Error(errMsg) 6 | } 7 | 8 | // SafenetworkJs libraries 9 | const Safenetworkjs = require('./safenetwork-api') 10 | 11 | // SafenetworkApi instance with SAFE Client Libs API 12 | const safeApi = require('./bootstrap') 13 | const safeJs = new Safenetworkjs.SafenetworkApi(safeApi) 14 | 15 | /* 16 | * Override window.fetch() in order to support safe:// URIs 17 | */ 18 | 19 | // Protocol handlers for fetch() 20 | const httpFetch = require('isomorphic-fetch') 21 | const protoFetch = require('proto-fetch') 22 | 23 | // map protocols to fetch() 24 | const fetch = protoFetch({ 25 | http: httpFetch, 26 | https: httpFetch, 27 | safe: safeJs.fetch.bind(safeJs) 28 | // https: Safenetwork.fetch.bind(Safenetwork), // Debugging with SAFE mock browser 29 | }) 30 | 31 | // SafenetworkApi class 32 | exports = module.exports = Safenetworkjs.SafenetworkApi 33 | module.exports.SafenetworkApi = Safenetworkjs.SafenetworkApi 34 | 35 | module.exports.safeJs = safeJs 36 | module.exports.protoFetch = protoFetch 37 | module.exports.WebIdProfile = require('./webid_profile') 38 | -------------------------------------------------------------------------------- /src/nfs-files.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Classes to track and access files in a SAFE NFS container 3 | * 4 | * Includes a file descriptor based interface to SAFE NFS files. 5 | * 6 | * Efficiencies 7 | * ------------ 8 | * These classes track file state between calls to the 9 | * SAFE NFS API, so when a client program is calling readFile() 10 | * for example, it doesn't result in calls to open/read/close 11 | * on the SAFE API for every readFile() call. 12 | * 13 | * It also caches the file state from a fetch() avoiding repeated 14 | * network GETs for the same information. 15 | * 16 | * Caching 17 | * ------- 18 | * File state is held in two caches (maps): 19 | * 1) A map of file descriptors to the file state object (class 20 | * NfsFileState) is application-wide, implemented by class _AllNfsFiles. 21 | * 2) A map of itemPath *within* a container to the SAFE NFS API File 22 | * objects in use by the container, implemented by class _NfsContainerFiles. 23 | * 24 | * Use of File Descriptors is Optional 25 | * ----------------------------------- 26 | * TODO: update this once finalised (I think everything here now uses file descriptors, and that you have to openFile/do stuff/closeFile at this level) 27 | * 28 | * Applcations don't have to use the file descriptor obtained 29 | * by calling openFile(), and may call the file operations 30 | * readFile(), writeFile() etc directly, in which case they must 31 | * pass the file descriptor as 'undefined'. 32 | * 33 | * Where the file has not already been opened with the correct permissions 34 | * readFile(), writeFile() etc will attempt to open it with the minimum 35 | * permissions they require, and leave it in this state. 36 | * 37 | * So file descriptors can be used, and result in slight efficiencies, 38 | * but it should also be performant to just call readFile(), writeFile() 39 | * and forget about openFile() and closeFile(). 40 | * 41 | * Usage by Container Classes 42 | * -------------------------- 43 | * A higher level class (e.g. NfsContainer) can use a _NfsContainerFiles 44 | * object to keep track of file paths to their file objects (and descriptors). 45 | * So that even if a client program doesn't make use of file descriptors, 46 | * they can be looked up and calls to the SAFE API minimised. 47 | * 48 | * Note that file descriptors are not from the SAFE API, but instead are 49 | * generated here. File descriptors are application-wide rather than per 50 | * container, but can only be used within the application that obtained them. 51 | * 52 | * Interim ref: 53 | * What in the SAFE API causes GET? 54 | * https://forum.safedev.org/t/what-in-the-api-causes-get/2008/5?u=happybeing 55 | */ 56 | 57 | const debug = require('debug')('safenetworkjs:file') // Web API 58 | const error = require('debug')('safenetworkjs:file E:') 59 | 60 | require('fast-text-encoding') // TextEncoder, TextDecoder (for desktop apps) 61 | 62 | // TODO remove when missing constants are available on safeApi.CONSTANTS 63 | const C = require('./constants') 64 | 65 | const minFileDescriptor = 1 66 | 67 | class _AllNfsFiles { 68 | /** 69 | * App wide map of file descriptors to open NfsFileState objects 70 | */ 71 | constructor () { 72 | this._map = {} // Use an object map rather than array because we expect fd's 73 | // to be sparse and don't want a big, largely empty array 74 | // when it should be fast to look up an item in a smallish map 75 | this._nextFileDescriptor = minFileDescriptor 76 | } 77 | 78 | newDescriptor (nfsFileState) { 79 | while (this._map[this._nextFileDescriptor] !== undefined) { 80 | debug('%s WARNING: file descriptor %s in use, next...', this.constructor._name, this._nextFileDescriptor) 81 | if (this._nextFileDescriptor < Number.MAX_SAFE_INTEGER) { 82 | this._nextFileDescriptor++ 83 | } else { 84 | this._nextFileDescriptor = minFileDescriptor 85 | } 86 | } 87 | this._map[this._nextFileDescriptor] = nfsFileState 88 | return this._nextFileDescriptor++ 89 | } 90 | 91 | // By deleting descriptors (called by close()) we reduce the chances of ever 92 | // running out. If app fails to close files, this might still happen eventually 93 | deleteDescriptor (fd) { 94 | delete this._map[fd] 95 | } 96 | 97 | getFileStateFromCache (fd) { 98 | return this._map[fd] 99 | } 100 | 101 | restoreFileDescriptorToCache (fd, fileState) { 102 | this._map[fd] = fileState 103 | } 104 | 105 | lookupFileStateForPath (itemPath) { 106 | this._map.forEach((fd) => { 107 | let fileState = this._map[fd] 108 | if (fileState._itemPath === itemPath) return fileState 109 | }) 110 | return undefined 111 | } 112 | } 113 | 114 | /** 115 | * Manage file state through different file operation sequences 116 | * 117 | * The following sequences use a file descriptor returned 118 | * by openFile() or createFile() to maintain state across the 119 | * series of operations. 120 | * 121 | * TODO: review and update the following which I think is not correct... 122 | * 123 | * Alternatively, an application can just make a single call to 124 | * readFile() or writeFile() which will perform the entire 125 | * sequence in one call. The same can be achieved with 126 | * createFile(), by passing data to the call. 127 | * 128 | * Existing file read/write: 129 | * ------------------------- 130 | * SafenetworkJs FS SAFE NFS API 131 | * 132 | * openFile() fetch(), open() 133 | * readFile() | writeFile() read() | write() 134 | * " " " " 135 | * closeFile() close(), update() 136 | * 137 | * Or alternatively, if you don't call openFile() first: 138 | * readFile() fetch(), open(), read(), close() 139 | * 140 | * And, again not calling openFile() first: 141 | * writeFile() fetch(), open(), write(), close(), update() 142 | * 143 | * Create and write a new file: 144 | * ---------------------------- 145 | * SafenetworkJs FS SAFE NFS API 146 | * 147 | * createFile() open() 148 | * writeFile() write() 149 | * " " 150 | * closeFile() close(), insert() 151 | * 152 | * Or if you pass the data to createFile(): 153 | * createFile() open(), write(), close(), insert() 154 | */ 155 | class NfsFileState { 156 | constructor (safeJs, itemPath, fileFetched, hasKey) { 157 | this._safeJs = safeJs // For safeJs.safeApi.CONSTANTS only 158 | this.hasKey = hasKey // Used to decide whether to insert() or update() the entry 159 | this._flags = undefined // When open, set to NFS flags (e.g. NFS_FILE_MODE_READ etc) 160 | this._writePos = undefined // Tracks file position next write 161 | this._isModified = false // Set true when a write is performed 162 | 163 | // File identity and state 164 | this._fileDescriptor = allNfsFiles.newDescriptor(this) // Always valid (but file may not be) 165 | this._itemPath = itemPath 166 | this._fileFetched = fileFetched // NFS File returned by fetch (existing file) 167 | this._fileOpened = undefined // NFS File returned by open (new or existing - but lacks size, version etc) 168 | this._versionOpened = undefined // Version when opened 169 | this._newMetaData = undefined // If set, will be written on closeFile() 170 | } 171 | 172 | fileDescriptor () { return this._fileDescriptor } // Positive integer unique per open file in this app 173 | 174 | setModified () { this._isModified = true } 175 | isModified () { return this._isModified } 176 | isDeletedFile () { return this.hasKey && !this._fileFetched } 177 | isOpen () { return this._fileOpened || this.isEmptyOpen } 178 | version () { return this._fileFetched ? this._fileFetched.version : undefined } 179 | 180 | isWriteable (flags) { 181 | if (flags === undefined) flags = this._flags 182 | return flags & this._safeJs.safeApi.CONSTANTS.NFS_FILE_MODE_OVERWRITE || 183 | flags & this._safeJs.safeApi.CONSTANTS.NFS_FILE_MODE_APPEND 184 | } 185 | 186 | /** 187 | * Release a file descriptor and forget cached file state 188 | * 189 | * Releases the descriptor for this object by deleting cached file state 190 | * (i.e. this NfsFileState object) from allNfsFiles. This should be 191 | * called whenever a writeFile() (or any other file operation) fails, to 192 | * ensure the FileState is refreshed in the case that failure was due to 193 | * another process/app modifying the file entry. 194 | */ 195 | releaseDescriptor () { 196 | if (this._fileDescriptor) { 197 | allNfsFiles.deleteDescriptor(this._fileDescriptor) 198 | this._fileDescriptor = undefined 199 | } 200 | } 201 | 202 | /** 203 | * Create NFS file for this object 204 | * 205 | * @param {Emulation} nfs emulation of MutableData 206 | * @return {Promise} C.SUCCESS or an Error object 207 | */ 208 | async create (nfs) { 209 | try { 210 | this._fileOpened = await nfs.open() 211 | if (this._fileOpened) { 212 | this._flags = this._safeJs.safeApi.CONSTANTS.NFS_FILE_MODE_OVERWRITE 213 | this._isModified = true 214 | this._versionOpened = 0 215 | this._writePos = 0 216 | return C.SUCCESS 217 | } 218 | } catch (e) { error(e); return e } 219 | 220 | return new Error('Unknown error creating NFS file') 221 | } 222 | 223 | /** 224 | * Open the NFS file of this object 225 | * 226 | * @param {Emulation} nfs emulation of MutableData 227 | * @param {Number} nfsFlags for NFS open() 228 | * @return {Promise} C.SUCCESS or an Error object 229 | */ 230 | async open (nfs, nfsFlags) { 231 | try { 232 | // this._fileOpened = this.isWriteable(nfsFlags) ? await nfs.open() : await nfs.open(this._fileFetched, nfsFlags) 233 | let opened 234 | let size 235 | this.isEmptyOpen = undefined 236 | if (this.isWriteable(nfsFlags)) { 237 | if (this._fileFetched) { 238 | opened = await nfs.open(this._fileFetched, nfsFlags) 239 | this._writePos = (nfsFlags === this._safeJs.safeApi.CONSTANTS.NFS_FILE_MODE_APPEND ? await this._fileFetched.size() : 0) 240 | } else { 241 | opened = await nfs.open() 242 | this._writePos = 0 // Writing to new empty file 243 | } 244 | } else { 245 | try { 246 | // NFS fails to open zero length files for read, so we must fake it 247 | size = await this._fileFetched.size() 248 | this.isEmptyOpen = (size === 0) 249 | debug('size: ', size) 250 | } catch (discard) {} 251 | 252 | debug('isEmptyOpen: %s', this.isEmtpyOpen) 253 | if (!this.isEmptyOpen) opened = await nfs.open(this._fileFetched, nfsFlags) 254 | } 255 | this._fileOpened = opened 256 | 257 | if (this._fileOpened || this.isEmptyOpen) { 258 | this._flags = nfsFlags 259 | this._versionOpened = this._fileOpened ? this._fileFetched.version : 0 260 | if (this.isWriteable()) { 261 | debug('opened (%s) for write', this._fileDescriptor) 262 | } else { 263 | debug('opened (%s) for read (size: %s)', this._fileDescriptor, size) 264 | } 265 | return C.SUCCESS 266 | } 267 | } catch (e) { error(e); return e } 268 | 269 | return new Error('Unknown error creating NFS file') 270 | } 271 | 272 | /** 273 | * Truncate an open-for-write file to size bytes (only implemented for size equal to zero) 274 | * 275 | * @param {Emulation} nfs emulation of MutableData 276 | * @param {Number} size (must be zero) 277 | * @return {Promise} C.SUCCESS or an Error object 278 | */ 279 | async _truncate (nfs, size) { 280 | try { 281 | if (size !== 0) throw new Error(this.constructor.name + '._truncate() not implemented for size other than zero') 282 | 283 | if (this._fileFetched) { 284 | let nfsFlags = this._safeJs.safeApi.CONSTANTS.NFS_FILE_MODE_OVERWRITE 285 | 286 | // Truncate by opening for overwrite, so if already open, close first 287 | let leaveClosed = this._fileOpened === undefined 288 | if (this._fileOpened) await this._fileOpened.close() 289 | 290 | this._fileOpened = await nfs.open(this._fileFetched, nfsFlags) 291 | this.setModified() 292 | this._writePos = 0 293 | 294 | if (leaveClosed) { 295 | // Close without releasing descriptor 296 | this._flags = undefined 297 | this._writePos = undefined 298 | await this._fileOpened.close() 299 | this._fileOpened = undefined 300 | } else if (this._fileOpened || this.isEmptyOpen) { 301 | this._flags = nfsFlags 302 | this._versionOpened = this._fileOpened ? this._fileOpened.version : 0 303 | } 304 | 305 | debug('truncated by (re)opening for overwrite') 306 | return C.SUCCESS 307 | } 308 | } catch (e) { error(e); return e } 309 | return C.SUCCESS 310 | } 311 | 312 | /** 313 | * Close the open NFS file of this object 314 | * 315 | * @param {Emulation} nfs emulation of MutableData 316 | * @return {Promise} C.SUCCESS or an Error object 317 | */ 318 | async close (nfs) { 319 | try { 320 | this.releaseDescriptor() 321 | this._flags = undefined 322 | // TODO should we discard this._fileFetched here (is version, size or other state valid?) 323 | if (this._fileOpened) await this._fileOpened.close() 324 | return C.SUCCESS 325 | } catch (e) { error(e); return e } 326 | return new Error('Unknown error closing NFS file') 327 | } 328 | } 329 | 330 | /** 331 | * Manage files for a SafeContainer (eg NfsContainer) 332 | * 333 | * Keeps a map of paths to SAFE NFS File objects, which have 334 | * either been fetched or created using the SAFE NFS API. 335 | * 336 | */ 337 | class _NfsContainerFiles { 338 | constructor (owner, safeJs, mData, nfs) { 339 | this._owner = owner 340 | this._safeJs = safeJs 341 | this._mData = mData 342 | this._nfs = nfs 343 | this._containerNfsFiles = {} // Map NFS container path to NFS fetched or created NFS File objects 344 | } 345 | 346 | nfs () { return this._nfs } 347 | 348 | getFileStateForDescriptor (fd) { 349 | return allNfsFiles.getFileStateFromCache(fd) 350 | } 351 | 352 | // Clear cache to force subsequent fetch() from network 353 | clearNfsFileFor (itemPath) { 354 | delete this._containerNfsFiles[itemPath] 355 | } 356 | 357 | _newFileState (itemPath, file, hasKey) { 358 | debug('%s._newFileState(\'%s\', %s, %s)', this.constructor.name, itemPath, file, hasKey) 359 | try { 360 | let fileState = new NfsFileState(this._safeJs, itemPath, file, hasKey) 361 | debug('fileState: %s', fileState) 362 | return fileState 363 | } catch (e) { error(e) } 364 | } 365 | 366 | /** 367 | * Release any file descriptor 368 | * @private 369 | * 370 | * @param {FileState} fileState 371 | * 372 | * Functions that are passed a file descriptor use that to get the 373 | * corresponding FileState object, and will call this function if an error 374 | * occurs during the file operation, which might sometimes be undesirable 375 | * because it invalidates the file descriptor, which means the client 376 | * can't retry the operation without first re-opening the file to get 377 | * a new file descriptor. So.. 378 | * 379 | * TODO consider accepting a SAFE API error code, and using that to decide 380 | * whether to invalidate the file descriptor. 381 | * 382 | * Note NFS fetched/created File objects remain in this._containerNfsFiles[] 383 | * and are never deleted. This could accumulate over time so the number of 384 | * these could be limited, and this would be a good place to purge the 385 | * cache of excess objects. So.. 386 | *\ 387 | * TODO implement limit on size of this._containerNfsFiles[] 388 | * based on time since last use. So each cache access must update a last 389 | * access time, and here we add code to purge the N least used entries 390 | * needed to bring the size of the cache down to a desired limit. Keeping 391 | * the cache small makes this relatively fast, while increasing the number 392 | * of fetch() operations if more than that number of files are open in 393 | * the current app at any one time. 394 | */ 395 | _destroyFileState (fileState) { 396 | fileState.releaseDescriptor() 397 | } 398 | 399 | /** 400 | * Fetch NFS File from cache of NFS File objects, or the network 401 | * 402 | * @param {String} itemPath A path (NFS entry key) 403 | * @param {Boolean} fromNetwork if true, get from network and update cache 404 | * @return {Promise} FileState if MD entry is active or deleted, undefined if there is no entry for itemPath 405 | */ 406 | async _fetchFileState (itemPath, fromNetwork) { 407 | debug('%s._fetchFileState(\'%s\', %s)', this.constructor.name, itemPath, fromNetwork) 408 | let fileState 409 | try { 410 | let file 411 | if (!fromNetwork) file = this._containerNfsFiles[itemPath] // Cached NFS fetch() File objects from fetch()/open() 412 | if (!file) { 413 | file = await this.nfs().fetch(itemPath) 414 | if (file) this._containerNfsFiles[itemPath] = file 415 | } 416 | if (file) { 417 | fileState = this._newFileState(itemPath, file, true /* hasKey */) 418 | debug('fileState fetched for: %s', itemPath) 419 | debug('fileState: %o', fileState) 420 | } else { 421 | debug('no entry found for: %s', itemPath) 422 | } 423 | return fileState 424 | } catch (e) { 425 | if (e.code === C.ERROR_CODE.ENCODE_DECODE_ERROR) { 426 | debug('deleted entry found: %s', itemPath) 427 | fileState = this._newFileState(itemPath, undefined, true /* hasKey */) 428 | return fileState // Deleted entry 429 | } else if (e.code === C.ERROR_CODE.NFS_FILE_NOT_FOUND) { 430 | debug('no entry found for: %s', itemPath) 431 | return undefined 432 | } 433 | error(e) 434 | throw e // Unexpected error so need to handle it in the above catch 435 | } 436 | } 437 | 438 | /** 439 | * Copy an immutable file (re-using existing immutable data) 440 | * 441 | * Supports copying a file within a single NFS container (not between 442 | * containers). 443 | * 444 | * @param {String} sourcePath 445 | * @param {String} destinationPath 446 | * @param {Boolean} copyMetadata [optional] if true, copies all file metadata (so like 'move') 447 | * @param {Boolean} ignoreVersion [optional] if true, overwrites destination even if last fetched version has changed 448 | * 449 | * @return {Promise} C.SUCCESS or an Error object 450 | */ 451 | async copyFile (sourcePath, destinationPath, copyMetadata, ignoreVersion) { 452 | debug('%s.copyFile(\'%s\', \'%s\')', this.constructor.name, sourcePath, destinationPath, copyMetadata) 453 | let result 454 | try { 455 | let srcFileState = await this._fetchFileState(sourcePath) 456 | let srcNfsFile = (srcFileState ? srcFileState._fileFetched : undefined) 457 | if (!srcNfsFile) { 458 | let err = new Error('copyFile() source file not found:', sourcePath) 459 | err.code = C.ERROR_CODE.NO_SUCH_ENTRY 460 | throw err 461 | } 462 | if (!copyMetadata) { 463 | // TODO need to clear and reset metadata in srcFileState._fileFetched before 464 | // Have asked for advice on how to do this: https://forum.safedev.org/t/implementing-nfs-api-rename/2109/5?u=happybeing 465 | } 466 | let perms // undefined means: if auth is needed, request default permissions 467 | let destFileState = await this._fetchFileState(destinationPath) 468 | if (!destFileState) { 469 | // Destination is a new file, so insert 470 | result = await this._safeJs.nfsMutate(this.nfs(), perms, 'insert', destinationPath, srcNfsFile) 471 | this._owner._handleCacheForCreateFileOrOpenWrite(destinationPath) 472 | } else { 473 | // Destination exists, so update 474 | let nfsIgnoreVersionConstant = 0 // TODO use NFS API constant when available 475 | let nfsVersion = (ignoreVersion ? destFileState.version() + 1 : nfsIgnoreVersionConstant) 476 | result = await this._safeJs.nfsMutate(this.nfs(), perms, 'update', destinationPath, srcNfsFile, nfsVersion) 477 | this._destroyFileState(destFileState) // New file so purge the cache 478 | this.clearNfsFileFor(destinationPath) // Flush cached NFS File object 479 | this._owner._handleCacheForChangedAttributes(destinationPath) 480 | } 481 | 482 | // After using the fetched file to update another entry, it takes on the version of the other, so needs refreshing 483 | this._destroyFileState(srcFileState) 484 | } catch (e) { error(e); return e } 485 | 486 | return result 487 | } 488 | 489 | /** 490 | * Move/rename a file (within the same container) 491 | * 492 | * Note: SafenetworkApi.moveFile() supports move/rename between containers 493 | * 494 | * @param {String} sourcePath 495 | * @param {String} destinationPath 496 | * 497 | * @return {Promise} Object { status: C.SUCCESS or an Error object 498 | * wasLastItem: true if itemPath folder left emtpy } 499 | */ 500 | async moveFile (sourcePath, destinationPath) { 501 | debug('%s.moveFile(\'%s\', \'%s\')', this.constructor.name, sourcePath, destinationPath) 502 | let status = C.SUCCESS 503 | let deleteResult 504 | try { 505 | if (await this.copyFile(sourcePath, destinationPath, true) === C.SUCCESS) { 506 | deleteResult = await this.deleteFile(sourcePath) 507 | if (deleteResult.status === C.SUCCESS) { 508 | debug('moveFile() succeeded') 509 | } 510 | } 511 | return deleteResult 512 | } catch (e) { 513 | error(e) 514 | status = e 515 | } 516 | 517 | return { status: status, wasLastItem: (deleteResult ? deleteResult.wasLastItem : undefined) } 518 | } 519 | 520 | /** 521 | * Open a file for read or write 522 | * 523 | * @param {String} itemPath 524 | * @param {Number} nfsFlags SAFE NFS API open() flags 525 | * 526 | * @return {Promise} Object { status: C.SUCCESS or an Error object 527 | * fileDescriptor: an integar >0 on success } 528 | */ 529 | async openFile (itemPath, nfsFlags) { 530 | debug('%s.openFile(\'%s\', %s)', this.constructor.name, itemPath, nfsFlags) 531 | let fileState 532 | let unknownError = this._safeJs.unknownErrorIn('openFile('+itemPath+')') 533 | try { 534 | if (itemPath.indexOf('/pack/tmp_pack_') !== -1) { 535 | debug('opening temp pack file!') 536 | } 537 | 538 | let result = unknownError 539 | fileState = await this._fetchFileState(itemPath) 540 | if (fileState) { 541 | debug('fileState: %o', fileState) 542 | result = await fileState.open(this.nfs(), nfsFlags) 543 | } 544 | if (result === C.SUCCESS) { 545 | // The File object returned by open lacks .version / .size of File object returned by fetch() 546 | // This should be fixed or documented, so try again with v0.9.1 and above (was 0.8.?) 547 | // Also the error is odd: currently when file open() for write, _fileOpened.size() gives strange error: '-1016: Invalid file mode (e.g. trying to write when file is opened for reading only)') 548 | debug('(%s) opened, size: ', fileState.fileDescriptor(), await fileState._fileFetched.size()) 549 | debug('fileState: %o', fileState) 550 | if (fileState.isWriteable()) this._owner._handleCacheForCreateFileOrOpenWrite(itemPath) 551 | return { status: C.SUCCESS, 'fileDescriptor': fileState.fileDescriptor() } 552 | } else { 553 | // Handle failure to open a file 554 | // Assume this is because it has been created but not yet closed (so doesn't exist on network) 555 | 556 | // At this point we have a new partialliy initialised FileState 557 | // rather than one which was opened for write 558 | if (fileState) { 559 | let lastFd = fileState.fileDescriptor() 560 | fileState.releaseDescriptor() // Discard partially initialised state 561 | // Its likely the preceding fd is what we want so see if it matches 562 | fileState = allNfsFiles.getFileStateFromCache(lastFd - 1) 563 | if (fileState && fileState._itemPath !== itemPath) fileState = undefined 564 | } 565 | 566 | // See if there is a FileState for itemPath as a new file... 567 | if (!fileState) fileState = allNfsFiles.lookupFileStateForPath(itemPath) 568 | 569 | if (fileState && fileState._fileOpened && fileState.isWriteable()) { 570 | debug('failed to open file. Assume because file is new, so closing before re-open') 571 | // Close to commit the file, then re-open for read and open for write 572 | let fdToReopen = fileState.fileDescriptor() 573 | result = await this._closeFile(itemPath, fdToReopen, true /* preserveFileState */) 574 | fileState._fileOpened = undefined 575 | if (result !== C.SUCCESS) { 576 | debug('openFile() failed (trying to close() the new file for re-open)') 577 | throw result 578 | } 579 | 580 | let file = await this.nfs().fetch(itemPath) 581 | this._containerNfsFiles[itemPath] = file 582 | if (file) { 583 | // (re)open for write/append on existing FileState to maintain fd for write 584 | fileState._fileFetched = file 585 | fileState._fileDescriptor = fdToReopen 586 | fileState._isModified = false 587 | fileState._fileOpened = undefined 588 | fileState._versionOpened = undefined 589 | result = await fileState.open(this.nfs(), this._safeJs.safeApi.CONSTANTS.NFS_FILE_MODE_APPEND) 590 | if (result === C.SUCCESS) { 591 | allNfsFiles.restoreFileDescriptorToCache(fdToReopen, fileState) 592 | } else { 593 | debug('openFile() failed to re-open new file, closed to allow read') 594 | throw result 595 | } 596 | 597 | let readFileState 598 | try { 599 | // open for read and return new fd 600 | readFileState = this._newFileState(itemPath, file, true /* hasKey */) 601 | result = unknownError 602 | if (readFileState) result = await readFileState.open(this.nfs(), nfsFlags) 603 | if (result === C.SUCCESS) { 604 | return { status: C.SUCCESS, 'fileDescriptor': readFileState.fileDescriptor() } 605 | } 606 | throw result 607 | } catch (e) { 608 | if (readFileState) readFileState.releaseDescriptor() // open() failed 609 | debug('openFile() failed trying to open new file for read (using readFileState)') 610 | throw e 611 | } 612 | } 613 | } 614 | 615 | throw unknownError 616 | } 617 | } catch (e) { 618 | if (fileState) fileState.releaseDescriptor() // open() failed 619 | debug(e) 620 | return e 621 | } 622 | } 623 | 624 | /** 625 | * Create a file. 626 | * 627 | * @param {String} itemPath 628 | * 629 | * @return {Promise} Object { status: C.SUCCESS or an Error object 630 | * fileDescriptor: an integar >0 on success } 631 | */ 632 | async createFile (itemPath) { 633 | debug('%s.createFile(\'%s\')', this.constructor.name, itemPath) 634 | let fileState 635 | try { 636 | fileState = await this._fetchFileState(itemPath, /* fromNetwork: */ true) 637 | if (fileState && fileState._fileFetched) { 638 | let err = new Error('createFile() failed - file exists') 639 | err.code = C.ERROR_CODE.DATA_EXISTS 640 | throw err 641 | } 642 | if (!fileState) fileState = this._newFileState(itemPath, undefined, /* hasKey */ false) 643 | 644 | if (fileState && (await fileState.create(this.nfs())) === C.SUCCESS) { 645 | debug('(%s) created: ', fileState.fileDescriptor(), itemPath) 646 | debug('fileState: %o', fileState) 647 | this._containerNfsFiles[itemPath] = fileState._fileOpened 648 | this._owner._handleCacheForCreateFileOrOpenWrite(itemPath) 649 | return { status: C.SUCCESS, 'fileDescriptor': fileState.fileDescriptor() } 650 | } else { 651 | throw this._safeJs.unknownErrorIn('createFile('+itemPath+')') 652 | } 653 | } catch (e) { 654 | if (fileState) { 655 | this._destroyFileState(fileState) 656 | this.clearNfsFileFor(itemPath) // Flush cached NFS File object 657 | } 658 | error(e) 659 | return { status: e } 660 | } 661 | } 662 | 663 | /** 664 | * Delete file 665 | * @param {String} itemPath 666 | * 667 | * @return {Promise} Object { status: C.SUCCESS or an Error object, 668 | * wasLastItem: true if itemPath folder left emtpy } 669 | */ 670 | async deleteFile (itemPath) { 671 | debug('%s.deleteFile(\'%s\')', this.constructor.name, itemPath) 672 | let fileState 673 | let result 674 | let wasLastItem = false 675 | try { 676 | fileState = await this._fetchFileState(itemPath, /* fromNetwork: */ true) 677 | 678 | if (fileState) { 679 | // POSIX unlink() decrements the file link count and removes 680 | // the file when it reaches zero unless the file is open, in 681 | // which case removal is delayed until close. 682 | // But we ignore the state which means for an open file the 683 | // file descriptor will become invalid, and any subsequent 684 | // operations on it will fail. 685 | 686 | let permissions // use defaults 687 | result = await this._safeJs.nfsMutate(this.nfs(), permissions, 'delete', 688 | itemPath, undefined, fileState.version() + 1) 689 | if (result === C.SUCCESS) { 690 | debug('deleted: ', itemPath) 691 | this._destroyFileState(fileState) // Purge from cache 692 | this.clearNfsFileFor(itemPath) // Flush cached NFS File object 693 | wasLastItem = await this._owner._handleCacheForDelete(itemPath) 694 | } else { 695 | throw result 696 | } 697 | } else { 698 | result = new Error('%s.deleteFile() - file not found: ', this.constructor.name, itemPath) 699 | result.code = C.ERROR_CODE.NO_SUCH_ENTRY 700 | } 701 | } catch (e) { 702 | if (fileState) this._destroyFileState(fileState) // Invalidate cached state 703 | debug(e) 704 | debug('deleteFile() failed on: ' + itemPath) 705 | result = e 706 | } 707 | return { status: result, 'wasLastItem': wasLastItem } 708 | } 709 | 710 | /** 711 | * Get user metadata for a file (file does not need to be open) 712 | * 713 | * @param {Number} fd [optional] file descriptor obtained from openFile() or createFile() 714 | * 715 | * 716 | * @return {Promise} Object { status: C.SUCCESS or an Error object 717 | * metadata: a buffer containing any metadata as previously set } 718 | */ 719 | async getFileMetadata (fd) { 720 | let result 721 | let fileState 722 | try { 723 | fileState = this.getFileStateForDescriptor(fd) 724 | if (fileState && fileState._fileFetched) { 725 | return { status: C.SUCCESS, 'metadata': fileState._fileFetched.userMetadata } 726 | } 727 | } catch (e) { error(e); result = e} 728 | 729 | return { status: this._safeJs.unknowErrorIn('getFileMetadata()'), 'metadata': fileState._fileFetched.userMetadata } 730 | } 731 | 732 | /** 733 | * Set metadata to be written when on closeFile() (for a file opened for write) 734 | * 735 | * Note: must only be called after succesful createFile() or openFile() for write 736 | * @param {String} itemPath 737 | * @param {Number} fd [optional] file descriptor 738 | * @param {Buffer} metadata Metadata that will be written on closeFile() 739 | * 740 | * @return {Promise} C.SUCCESS or an Error object 741 | */ 742 | setFileMetadata (itemPath, fd, metadata) { 743 | let result 744 | try { 745 | let fileState = this.getFileStateForDescriptor(fd) 746 | if (fileState) fileState._newMetadata = metadata 747 | this._owner._handleCacheForChangedAttributes(itemPath) 748 | return C.SUCCESS 749 | } catch (e) { error(e); return e } 750 | } 751 | 752 | /** 753 | * Read up to len bytes starting from pos 754 | * 755 | * This function can be used in one of two ways: 756 | * - simple: just call readFile() and it will read data, and if the 757 | * file is not open yet, it will do that first 758 | * - you can call openFile() before, to open in a specific mode using flags 759 | * 760 | * Note: if this function fails, the cached file state is purged and any file 761 | * descriptor will be invalidated 762 | * 763 | * @param {String} itemPath path (key) of the file (in container which owns this _NfsContainerFiles) 764 | * @param {Number} fd [optional] file descriptor obtained from openFile() 765 | * @param {Number} pos (Number | C.NFS_FILE_START) 766 | * @param {Number} len (Number | C.NFS_FILE_END) 767 | * 768 | * @return {Promise} Object { status: C.SUCCESS or an Error object, 769 | * content: String containing the bytes read } 770 | */ 771 | async readFile (itemPath, fd, pos, len) { 772 | debug('%s.readFile(\'%s\', %s, %s, %s)', this.constructor.name, itemPath, fd, pos, len) 773 | if (pos === undefined) pos = this._safeJs.safeApi.CONSTANTS.NFS_FILE_START 774 | if (len === undefined) len = this._safeJs.safeApi.CONSTANTS.NFS_FILE_END 775 | 776 | let fileState 777 | try { 778 | fileState = this.getFileStateForDescriptor(fd) 779 | if (!fileState) { 780 | let err = new Error('(' + fd + ') readFile() ERROR - invalid file descriptor') 781 | err.code = C.ERROR_CODE.INVALID_FILE_DESCRIPTOR 782 | throw err 783 | } 784 | 785 | let content = '' 786 | let size = await fileState._fileFetched.size() 787 | if (pos + len > size) len = size - pos 788 | if (len > 0 && fileState._fileOpened) { 789 | content = await fileState._fileOpened.read(pos, len) 790 | let decoder = new TextDecoder() 791 | content = decoder.decode(content) 792 | } 793 | debug('(%s) %s bytes read', fd, content.length) 794 | 795 | return { status: C.SUCCESS, 'content': content } 796 | } catch (e) { 797 | if (fileState) this._destroyFileState(fileState) // read() failed 798 | debug(e) 799 | return { status: e } 800 | } 801 | } 802 | 803 | /** 804 | * Read up to len bytes into buf (Uint8Array), starting at pos 805 | * 806 | * This function can be used in one of two ways: 807 | * - simple: just call readFileBuf() and it will read data, and if the 808 | * file is not open yet, it will do that first 809 | * - you can call openFile() before, to open in a specific mode using flags 810 | * 811 | * Note: if this function fails, the cached file state is purged and any file 812 | * descriptor will be invalidated 813 | * 814 | * @param {String} itemPath path (key) of the file (in container which owns this _NfsContainerFiles) 815 | * @param {Number} fd [optional] file descriptor obtained from openFile() 816 | * @param {Uint8Array} buf [description] 817 | * @param {Number} pos (Number | CONSTANTS.NFS_FILE_START) 818 | * @param {Number} len (Number | CONSTANTS.NFS_FILE_END) 819 | * @return {Promise} Object { status: C.SUCCESS or an Error object, 820 | * bytes: Integer number of bytes read } 821 | */ 822 | async readFileBuf (itemPath, fd, buf, pos, len) { 823 | debug('%s.readFileBuf(\'%s\', %s, buf, %s, %s)', this.constructor.name, itemPath, fd, pos, len) 824 | if (pos === undefined) pos = this._safeJs.safeApi.CONSTANTS.NFS_FILE_START 825 | if (len === undefined) len = this._safeJs.safeApi.CONSTANTS.NFS_FILE_END 826 | 827 | let fileState 828 | try { 829 | fileState = this.getFileStateForDescriptor(fd) 830 | if (!fileState) throw new Error('(' + fd + ') readFileBuf() ERROR - invalid file descriptor') 831 | 832 | // Attempt to read a file that has just been created will give size 0: 833 | let size = fileState._fileFetched ? await fileState._fileFetched.size() : 0 834 | if (pos + len > size) len = size - pos 835 | if (len > 0 && fileState._fileOpened) { 836 | let readBuf = await fileState._fileOpened.read(pos, len) 837 | size = readBuf.byteLength 838 | buf.set(readBuf) 839 | } else { 840 | size = 0 841 | } 842 | 843 | debug('%s bytes read from file.', size) 844 | return { status: C.SUCCESS, 'bytes': size } 845 | } catch (e) { 846 | if (fileState) this._destroyFileState(fileState) // read() failed 847 | debug(e) 848 | return { status: e } 849 | } 850 | } 851 | 852 | /** 853 | * Write up to len bytes starting from pos 854 | * 855 | * This function can be used in one of two ways: 856 | * - simple: just call writeFile() and it will write data, and if the 857 | * file is not open yet, it will do that first 858 | * - you can call openFile() before, to open in a specific mode using flags 859 | * 860 | * Note: if this function fails, the cached file state is purged and any file 861 | * descriptor will be invalidated 862 | * 863 | * @param {String} itemPath path (key) of the file (in container which owns this _NfsContainerFiles) 864 | * @param {Number} fd [optional] file descriptor obtained from openFile() 865 | * @param {Buffer|String} content (Number | CONSTANTS.NFS_FILE_END) 866 | * 867 | * @return {Promise} Object { status: C.SUCCESS or an Error object, 868 | * bytes: Integer number of bytes written } 869 | */ 870 | async writeFile (itemPath, fd, content) { 871 | debug('%s.writeFile(\'%s\', %s, \'%s\')', this.constructor.name, itemPath, fd, content) 872 | let fileState 873 | try { 874 | fileState = this.getFileStateForDescriptor(fd) 875 | if (!fileState) throw new Error('(' + fd + ') writeFile() ERROR - invalid file descriptor') 876 | 877 | let bytes = content.length 878 | await fileState._fileOpened.write(content) 879 | fileState.setModified() 880 | fileState._writePos += bytes 881 | debug('%s bytes written to file.', bytes) 882 | return { status: C.SUCCESS, 'bytes': size } 883 | } catch (e) { 884 | if (fileState) this._destroyFileState(fileState) 885 | debug(e) 886 | return { status: e } 887 | } 888 | } 889 | 890 | /** 891 | * Write to file, len bytes from buf (Uint8Array) 892 | * 893 | * This function can be used in one of two ways: 894 | * - simple: just call writeFileBuf() and it will write data, and if the 895 | * file is not open yet, it will do that first 896 | * - you can call openFile() before, to open in a specific mode using flags 897 | * 898 | * Note: if this function fails, the cached file state is purged and any file 899 | * descriptor will be invalidated 900 | * 901 | * @param {String} itemPath path (key) of the file (in container which owns this _NfsContainerFiles) 902 | * @param {Number} fd [optional] file descriptor obtained from openFile() 903 | * @param {Uint8Array} buf [description] 904 | * @param {Number} len 905 | * @param {Number} pos [optional] position of file to write (must not be less than end of last write) 906 | * 907 | * @return {Promise} Object { status: C.SUCCESS or an Error object, 908 | * bytes: Integer number of bytes written } 909 | */ 910 | async writeFileBuf (itemPath, fd, buf, len, pos) { 911 | debug('%s.writeFileBuf(\'%s\', %s, buf, %s, %s)', this.constructor.name, itemPath, fd, len, pos) 912 | let fileState 913 | try { 914 | fileState = this.getFileStateForDescriptor(fd) 915 | if (!fileState) throw new Error('(' + fd + ') writeFileBuf() ERROR invalid file descriptor') 916 | 917 | if (fd === 55) { 918 | debug('fd: ', fd) 919 | } 920 | 921 | if (pos && pos !== fileState._writePos) { 922 | debug('MISMATCHED POS: %s WRITE POS: %s', pos, fileState._writePos) 923 | // TODO insert code to write nulls to fill gap if pos < writePos 924 | let padlen = fileState._writePos - pos 925 | if (padlen > 0) { 926 | let padbuf = Buffer.alloc(padlen) // Inits with zeros 927 | await fileState._fileOpened.write(padbuf) 928 | fileState._writePos += padlen 929 | } 930 | } 931 | 932 | await fileState._fileOpened.write(buf.slice(0, len)) 933 | fileState._writePos += len 934 | fileState.setModified() 935 | debug('%s bytes written to file.', len) 936 | return { status: C.SUCCESS, 'bytes': len } 937 | } catch (e) { 938 | if (fileState) this._destroyFileState(fileState) 939 | debug(e) 940 | return { status: e } 941 | } 942 | } 943 | 944 | /** 945 | * Truncate a file to size bytes (only implements size === 0) 946 | * 947 | * @private This function is implemented purely to allow FUSE to open a 948 | * file for append, but overwrite it by first truncating its size to zero. 949 | * This is needed because POSIX open() only has flags for write, not for 950 | * append. But since SAFE NFS lacks file truncate, we can only truncate 951 | * to zero which we do be creating a new file with NFS open(). 952 | * 953 | * When opening a SAFE NFS file for write we must 'append', otherwise FUSE 954 | * would have now way to append (since it can only open() for write, not 955 | * write with append). In turn, the ony way to allow FUSE to be able to open 956 | * and overwrite an existing NFS file is to implement truncate at size zero. 957 | * 958 | * @param {String} itemPath 959 | * @param {Number} fd [optional] if omitted, truncates based on itemPath 960 | * @param {Number} size 961 | * 962 | * @return {Promise} C.SUCCESS or an Error object 963 | */ 964 | async _truncateFile (itemPath, fd, size) { 965 | debug('%s._truncateFile(\'%s\', %s, %s)', this.constructor.name, itemPath, fd, size) 966 | let fileState 967 | try { 968 | if (size !== 0) throw new Error(this.constructor.name + '.truncateFile() not implemented for size other than zero') 969 | if (fd !== undefined) { 970 | fileState = this.getFileStateForDescriptor(fd) 971 | if (fileState) { 972 | // Get state before it is invalidated by fileState.close() 973 | await fileState._truncate(this.nfs(), size) 974 | this._owner._handleCacheForChangedAttributes(itemPath) 975 | this.clearNfsFileFor(itemPath) // Flush cached NFS File object 976 | return C.SUCCESS 977 | } 978 | } else { 979 | // When a file is truncated without specifying an open descriptor 980 | // we truncate any open-for-write NFS Files for this path 981 | await this.__truncateOpenFiles(itemPath, size) 982 | this._owner._handleCacheForChangedAttributes(itemPath) 983 | this.clearNfsFileFor(itemPath) // Flush cached NFS File object 984 | return C.SUCCESS 985 | } 986 | } catch (e) { 987 | // close/insert/update failed so invalidate cached state 988 | if (fileState) this._destroyFileState(fileState) 989 | this._owner._handleCacheForChangedAttributes(itemPath) 990 | error(e) 991 | return e 992 | } 993 | } 994 | 995 | /** 996 | * Truncate all open-for-write NFS fetchedFile objects for itemPath 997 | * 998 | * @param {String} itemPath 999 | * @param {Number} size (must be zero) 1000 | * 1001 | * @return {Promise} C.SUCCESS or an Error object 1002 | */ 1003 | async __truncateOpenFiles (itemPath, size) { 1004 | debug('%s.__truncateOpenFiles(\'%s\', %s)', this.constructor.name, itemPath, size) 1005 | try { 1006 | let keys = Object.keys(allNfsFiles._map) 1007 | for (let i = 0, len = keys.length; i < len; i++) { 1008 | let fileState = allNfsFiles._map[keys[i]] 1009 | if (fileState._itemPath === itemPath) { 1010 | let result = await fileState._truncate(this.nfs(), size) 1011 | if (result !== C.SUCCESS) throw result 1012 | } 1013 | } 1014 | return C.SUCCESS 1015 | } catch (e) { 1016 | error(e) 1017 | return e 1018 | } 1019 | } 1020 | 1021 | /** 1022 | * Close file and save to network 1023 | * @param {String} itemPath 1024 | * @param {Number} fd 1025 | * 1026 | * @return {Promise} Object { status: C.SUCCESS or an Error object } 1027 | */ 1028 | async closeFile (itemPath, fd) { 1029 | debug('%s.closeFile(\'%s\', %s)', this.constructor.name, itemPath, fd) 1030 | return this._closeFile(itemPath, fd, false) 1031 | } 1032 | 1033 | /** 1034 | * @Private 1035 | * Close file and save to network 1036 | * @param {String} itemPath 1037 | * @param {Number} fd 1038 | * @param {boolean} preserveFileState (unless close fails) 1039 | * 1040 | * @return {Promise} Object { status: C.SUCCESS or an Error object } 1041 | */ 1042 | async _closeFile (itemPath, fd, preserveFileState) { 1043 | debug('%s._closeFile(\'%s\', %s)', this.constructor.name, itemPath, fd) 1044 | let fileState 1045 | let result = C.SUCCESS 1046 | try { 1047 | if (fd !== undefined) { 1048 | fileState = this.getFileStateForDescriptor(fd) 1049 | } 1050 | 1051 | if (fileState && fileState.isOpen()) { 1052 | // Get state before it is invalidated by fileState.close() 1053 | let isModified = fileState.isModified() 1054 | let version = fileState.version() 1055 | await fileState.close(this.nfs()) 1056 | if (isModified) { 1057 | this.clearNfsFileFor(itemPath) // Flush cached NFS File object 1058 | this._owner._handleCacheForChangedAttributes(itemPath) 1059 | let permissions // use defaults 1060 | debug('doing %s(\'%s\')', fileState.hasKey ? 'update' : 'insert', itemPath) 1061 | result = await this._safeJs.nfsMutate(this.nfs(), permissions, (fileState.hasKey ? 'update' : 'insert'), 1062 | fileState._itemPath, fileState._fileOpened, version + 1, fileState._newMetadata) 1063 | } 1064 | if (!preserveFileState) this._destroyFileState(fileState) // Invalidate cached state after closeFile() 1065 | } 1066 | if (result !== C.SUCCESS) throw this._safeJs.unknownErrorIn('_closeFile('+itemPath+')') 1067 | 1068 | return result 1069 | } catch (e) { 1070 | // close/insert/update failed so invalidate cached state 1071 | if (fileState) this._destroyFileState(fileState) 1072 | error(e) 1073 | if (e.code === C.ERROR_CODE.LOW_BALANCE) { 1074 | this._safeJs.enableLowBalanceWarning() 1075 | } 1076 | return e 1077 | } 1078 | } 1079 | } 1080 | 1081 | const allNfsFiles = new _AllNfsFiles() 1082 | 1083 | module.exports.allNfsFiles = allNfsFiles 1084 | module.exports.NfsFileState = NfsFileState 1085 | module.exports._NfsContainerFiles = _NfsContainerFiles 1086 | -------------------------------------------------------------------------------- /src/safe-containers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Container wrapper classes for SAFE MutableData: 3 | * SafeContainer 4 | * PublicContainer (_public) 5 | * PrivateContainer (_music, _documents etc) 6 | * PublicNamesContainer (_publicNames) 7 | * ServicesContainer 8 | * NfsContainer 9 | */ 10 | 11 | require('fast-text-encoding') // TextEncoder, TextDecoder (for desktop apps) 12 | 13 | const debug = require('debug')('safenetworkjs:containers') 14 | const error = require('debug')('safenetworkjs:containers E:') 15 | const debugEntry = require('debug')('safenetworkjs:container-entries') 16 | const debugCache = require('debug')('safenetworkjs:cache') 17 | 18 | const u = require('./safenetwork-utils') 19 | const _NfsContainerFiles = require('./nfs-files')._NfsContainerFiles 20 | const C = require('./constants') 21 | 22 | const defaultContainerNames = [ 23 | '_documents', 24 | '_downloads', 25 | '_music', 26 | // '_pictures', See https://github.com/maidsafe/safe_client_libs/issues/680 27 | '_public', 28 | '_publicNames', 29 | '_videos' 30 | ] 31 | 32 | const containerTypeCodes = { 33 | defaultContainer: 'default-container', // SAFE default container (e.g. _public, _publicNames etc) 34 | nfsContainer: 'nfs-container', // an NFS container, or if in a default container an entry that ends with slash 35 | file: 'file', // an entry that doesn't end with slash 36 | newFile: 'new-file', // created, awaiting closeFile(), not yet stored in container 37 | fakeContainer: 'fake-container', // a path ending with '/' that matches part of an entry (default is we fake attributes of a container) 38 | lt: 'deleted-entry', // entry exists, but value has been deleted() 39 | servicesContainer: 'services-container', // Services container for a public name 40 | service: 'service', // Services container for a public name 41 | childContainerItem: 'child-container-item', // Need to get type from child container 42 | notFound: 'not-found', // Item not found in container 43 | notValid: 'not-valid' 44 | } 45 | 46 | /** 47 | * Check if results for this container type should be cached 48 | * @param {String} containerType a value from containerTypeCodes 49 | * @return {Boolean} true if the type should have cached results 50 | */ 51 | function isCacheableResult (fileOperation, operationResult) { 52 | if (process.env.SAFENETWORKJS_CACHE === 'disable') return false 53 | 54 | if (fileOperation === 'itemAttributes') { 55 | let containerType = operationResult.entryType 56 | // We don't store/update times for directories, instead always use 'now' 57 | return containerType === containerTypeCodes.file || 58 | containerType === containerTypeCodes.newFile 59 | } 60 | if (fileOperation === 'listFolder') { 61 | return true 62 | } 63 | return false 64 | } 65 | 66 | /** 67 | * A cache used by the filesystem (FS) interface methods of SafeContainer 68 | * 69 | * More than just a cache, this tracks the parent child entries which 70 | * means that when a container is accessed via the cache it is gauranteed 71 | * to be up to date (e.g. if the entry in the parent has changed). 72 | * 73 | * A container is invalid if it has a different version from its entry in its 74 | * parent container. 75 | * 76 | * On access, an invalid container will be re-validated and any child containers 77 | * present in the cache will be removed. So a container returned from the 78 | * cache will always be valid. 79 | * 80 | * The cache and FS interface work with standard SAFE containers and with 81 | * stand-alone containers (created from an XOR address) so long as they 82 | * are ALWAYS created with a unique containerPath, i.e. its place in the 83 | * heirarchy, because the containerPath is used to index the container 84 | * in the ContainerMap. 85 | * 86 | * Implementation 87 | * -------------- 88 | * The cache is a simple map of containerPath to container object. 89 | * 90 | */ 91 | class ContainerMap { 92 | constructor () { 93 | this._map = [] 94 | } 95 | 96 | put (containerPath, container) { 97 | this._map[containerPath] = container 98 | } 99 | 100 | async get (containerPath) { 101 | let container 102 | try { 103 | container = this._map[containerPath] 104 | if (container && container._parent) { 105 | container.initialise() // Checks container._entryValueVersion valid, if not will update 106 | } 107 | } catch (e) { 108 | this._map[containerPath] = undefined 109 | container = undefined 110 | error(e) 111 | } 112 | return container 113 | } 114 | } 115 | 116 | const containerMap = new ContainerMap() 117 | 118 | /** 119 | * Base class for SAFE containers 120 | * 121 | * Defaults are set for _public 122 | */ 123 | class SafeContainer { 124 | /** 125 | * A template class for creating and managing SAFE containers, each a wrapper for a mutable data 126 | * 127 | * The container classes provide simplified APIs for accessing mutable 128 | * data, including a filesystem (FS) style interface for the standard 129 | * SAFE container types. 130 | * 131 | * SafeContainer is a template which is extended to create classes 132 | * which handle the SAFE default containers (_public, _publicNames), 133 | * services containers, and NFS containers. 134 | * 135 | * You can also access 'stand-alone containers' via an XOR address (see below) 136 | * 137 | * All instantiated containers are held within the containerMap which 138 | * gaurantees any returned container is valid (e.g. that if it has 139 | * a parent container, the parent entry has not changed). 140 | * 141 | * Stand-alone Containers 142 | * ---------------------- 143 | * A stand-alone container is not a default container *and* has no parent 144 | * container. 145 | * 146 | * If you wish to use the file system (FS) interface with a stand-alone 147 | * container you MUST provide a containerPath on creation, and this 148 | * must end with the XOR address of the container being created. For 149 | * example: '/mds/'. You can then use the FS 150 | * interface as implemented in these classes or derive your own 151 | * class to override those operations. 152 | * 153 | * @param {Object} safeJs SafenetworkApi (owner) 154 | * @param {String} containerName the name of the container (e.g. '_public') or an XOR address if tagType is defined 155 | * @param {String} containerPath where this container appears in the SAFE container tree (e.g. '/_public', '/mds') 156 | * @param {String} subTree [optional] undefined or '' to mount the whole container, or a sub-tree of the container being 'mounted' 157 | * @param {String} parent [optional] parent container 158 | * @param {String} parentEntryKey [optional] key for this container's entry within parent container 159 | * @param {Number} tagType [optional] mutable data tag_type (required when containerName is an XOR address) 160 | * 161 | * NOTE: The terms 'mount' and 'mounted' are used loosely, to indicate the 162 | * effect rather than a filesystem style mount. 163 | */ 164 | constructor (safeJs, containerName, containerPath, subTree, parent, parentEntryKey, tagType) { 165 | this._safeJs = safeJs // SafenetworkJs instance 166 | this._name = containerName 167 | this._containerPath = containerPath + (u.isNfsFolder(containerPath) ? '' : '/') 168 | if (!subTree) subTree = '' 169 | this._subTree = subTree + (subTree.length && !u.isNfsFolder(subTree) ? '/' : '') 170 | this._parent = parent 171 | this._parentEntryKey = parentEntryKey 172 | this._tagType = tagType 173 | 174 | /** 175 | * File System Results Cache 176 | * 177 | * Each container maintains a cache of the last result for certain file 178 | * operations, such as itemAttributes. 179 | * 180 | * This is implemented using indirection, so that the cache can be accessed 181 | * modified and cleared by external processes, and not just the container 182 | * itself. 183 | * 184 | * Indirection allows a single cached result to be accessed and relied 185 | * upon by clients, including the parent container which caches results 186 | * that it supplies, while also being able to access the cached results 187 | * that come from a child container (e.g. parent as a default container 188 | * such as `_public`, child as an NFS container accessed via `_public`) 189 | * 190 | * Indirection is available through 'ResultsRef' objects, which are 191 | * obtained by a client process (or parent container accessing a child) 192 | * using the 'ResultsRef' version of an access function. So where 193 | * caching is supported there will be both a simple access function 194 | * such as itemAttributes() and an itemAttributesResultsRef() alternative 195 | * which returns the same result, but wrapped as a ResultsRef. 196 | * 197 | * Currently caching is supported by both variants, but neither use 198 | * the cache. So a process wanting to access cached values must do 199 | * this by keeping track of ResultsRef objects and using the value 200 | * from that when available, and if not, then executing the file 201 | * operation instead. 202 | * 203 | * ResultsRef 204 | * All containers keep their own map of ResultsRef objects, and if 205 | * a container has a child, this allows it to support caching even 206 | * if it isn't the source of the cached result. 207 | * 208 | * Each ResultsRef object contains a reference to a second object, a 209 | * resultsMap, and the key to use when looking up a result in that 210 | * resultsMap. The lookup returns a ResultHolder object, which contains 211 | * up to one object per file operation at a filesystem location. 212 | * 213 | * So to lookup the cached result for itemAttributes(itemPath) is in essence: 214 | * let resultsRef = this._resultsRefMap[itemPath] 215 | * if (resultsRef) resultHolder = resultsRef.resultsMap[resultsRef.resultsKey] 216 | * if (resultHolder) result = resultHolder['itemAttributes'] 217 | * 218 | * The 'key' identifies the filesystem location (itemPath) relative to the 219 | * container which owns the resultsMap. 220 | * 221 | * ResultHolder 222 | * Each ResultHolder contains one or more results for a given location 223 | * mapped by the name of the filesystem operation. 224 | * 225 | * So resultHolder['itemAttributes'] returns the value that was last 226 | * returned by itemAttributes() for a given location (the key used to 227 | * look up the resultHolder in the resultsRefMap). 228 | * 229 | * A client process can safely add information to the resultHolder to 230 | * save regenerating it from the result each time it accesses the cache. 231 | * So, for example, FUSE getattr() uses the result from itemAttributes() 232 | * to create the result for getatter(), and can cache this by assigning 233 | * it to the ResultHolder, as `resultHolder['getattr'] = fuseResult`. It 234 | * can do this, because if anything needs to invalidate the cached result 235 | * it will delete the resultHolder object, and all users of the object 236 | * such as a client process, or a parent container, will find that it 237 | * no longer exists when they try to access it. 238 | * 239 | * So, to summarise: 240 | * - a client wanting to use caching, calls 'ResultsRef' methods 241 | * such as itemAttributesResultsRef() rather than itemAttributes(). 242 | * Note that itemAttributes() itself uses itemAttributesResultsRef(), 243 | * so it is ok to mix the two. Calling itemAttributes() still 244 | * creates or updates the cached ResultHolder, it just doesn't 245 | * return a ResultsRef. 246 | * 247 | * - the client must keep track of the ResultsRefs it obtains, and 248 | * can use them to obtain the ResultHolder for a location (itemPath). 249 | * To do so, a client (including a parent container in SafenetworkJs) 250 | * keeps a resultsRefMap, and can look up a ResultsRef using 251 | * resultsRefMap[itemPath]. It then uses resultsRef.resultsMap and 252 | * resultsRef.resultsKey to obtain the ResultHolder for that itemPath. 253 | * 254 | * - anyone with a ResultHolder (client, parent container etc) can 255 | * invalidate a cached result by deleting the ResultHolder (clearing 256 | * all file operation results), or deleting a specific file operation 257 | * result from a ResultHolder. 258 | * 259 | * So either: `delete resultMap[resultsKey]` 260 | * Or: `delete resultHolder['itemAttributes']` 261 | * 262 | * NOTES: 263 | * At the present time, the cache is NOT used by SafenetworkJs 264 | * operations. So if you call a SafenetworkJs operation it will always 265 | * access the SAFE API, and ignore any cached results. This may change 266 | * in future, possibly on an opt-in basis (i.e by the application 267 | * requesting the cache be used in order to speed things up with 268 | * minimum effort by the application itself). 269 | */ 270 | 271 | /** 272 | * Map of itemPath to resultsRef 273 | * 274 | * Looked up using an itemPath, the resultsRef for holds: 275 | * - resultsMap // SafenetworkJs' map of results for a given container key 276 | * - key // The key to look up the relevant resultsHolder 277 | * 278 | * @type {Array} 279 | */ 280 | this._resultsRefMap = [] // Maintained by ResultsRef functions, e.g. itemAttributesResultsRef() 281 | 282 | this._resultHolderMap = [] // Filesystem results cached by operation and container key 283 | // Cache result holder objects are accessible to user app 284 | // through ResultsRef() functions, but not 285 | // used internally yet. 286 | // For cached operations, if a container obtains the result 287 | // from a child, it uses the child's ResultsRef function to 288 | // obtain the resultHolder, and uses that. So there will 289 | // only ever be one resultHolder per path, used by both 290 | // the parent and child container. 291 | } 292 | 293 | /** 294 | * Initialise by accessing a SAFE container or existing MutableData on the network 295 | * 296 | * Also called by ContainerMap to validate existing child container up 297 | * to date with its entry in a parent 298 | * 299 | * See also create() 300 | * 301 | * @return {Promise} the MutableData for this container 302 | */ 303 | async initialise () { 304 | if (this._mData) return this._mData 305 | debug('%s.initialise() for container _name: %s at _containerPath: %s', this.constructor.name, this._name, this._containerPath) 306 | 307 | try { 308 | if (this.isDefaultContainer()) { 309 | // TODO check we have the desired permissions and if not request them 310 | debug('auth.getContainer(%s)', this._name) 311 | this._mData = await this._safeJs.auth.getContainer(this._name) 312 | } else if (this._parent && this._parentEntryKey) { 313 | // Get xor address from parent entry 314 | debug('getting xor address from parent entry key: %s', this._parentEntryKey) 315 | let valueVersion = await this._parent.getValueVersion(this._parentEntryKey) 316 | if (!this._entryValueVersion || this._entryValueVersion.version !== valueVersion.version) { 317 | // Invalidated or not yet initialised 318 | this._mData = await this._initialiseFromXorName(valueVersion.buf) 319 | if (this._mData) this._entryValueVersion = valueVersion 320 | } 321 | } else { 322 | this._mData = await this._initialiseFromXorName(this._name.buf, this._tagType) 323 | } 324 | 325 | if (this._mData) await this.initialiseNfs() 326 | 327 | // Add to FS cache if it has a containerPath 328 | if (this._containerPath !== '') containerMap.put(this._containerPath, this) 329 | } catch (e) { 330 | let msg = 'SafeContainer initialise failed: ' + this._name + ' (' + e.message + ')' 331 | debug(msg + '\n' + e.stack) 332 | throw new Error(msg) 333 | } 334 | } 335 | 336 | async _initialiseFromXorName (xorName, tagType) { 337 | debug('%s._initialiseFromXorName(%s, %s)', xorName, tagType) 338 | if (tagType === undefined) tagType = this._tagType 339 | try { 340 | if (tagType === undefined) throw new Error(this.constructor.name + '._initialiseFromXorName() - tagType not defined') 341 | this._name = xorName 342 | return this._safeJs.getMdFromHash(this._name, tagType) 343 | } catch (e) { error(e) } 344 | } 345 | 346 | create () { 347 | // Template only - should be implemented in extender 348 | // class (i.e. NfsContainer, ServicesContainer etc) to 349 | // ensure any parent is updated correctly 350 | throw new Error(this.constructor.name + '.create() - TODO (not yet implemented in extener class)') 351 | } 352 | 353 | // SafeContainer assumes it is a default container, so override this in non-default container classes 354 | isDefaultContainer () { 355 | // Catch call on non-default container due to lack of override: 356 | if (this._parent) throw new Error(this.constructor.name + '.isDefaultContainer() - TODO (not yet implemented in extener class)') 357 | 358 | return true 359 | } 360 | 361 | isPublic () { return true } 362 | isSelf (itemPath) { return itemPath === '' } // Empty path is equivalent to '.' 363 | 364 | // TODO implement isValidKey() here for default folders, and override in other containers where needed 365 | isValidKey (key) { return true } // Containers should sanity check keys in case of corruption 366 | // but still cope with a valid key that has an invalid value 367 | isHiddenEntry (key) { 368 | return key === C.MD_METADATA_KEY || // Containers for which this is not hidden should override 369 | !this.isValidKey(key) 370 | } 371 | 372 | isDeletedEntryValue (entryValue) { 373 | return entryValue.buf.byteLength === 0 374 | } 375 | 376 | // Check if key exists and is not deleted 377 | async isActiveKey (itemPath) { 378 | try { 379 | let entryValue = await this._mData.get(itemPath) 380 | if (!entryValue || this.isDeletedEntryValue(entryValue)) return false 381 | return true 382 | } catch (e) { 383 | error(e) 384 | return false 385 | } 386 | } 387 | 388 | encryptKeys () { return this._encryptKeys === true } 389 | encryptValues () { return this._encryptValues === true } 390 | 391 | async initialiseNfs () {} // Override in classes which support NFS 392 | 393 | async updateMetadata () { 394 | let metadata = { 395 | size: 0, 396 | version: -1 397 | } 398 | 399 | try { 400 | metadata = { 401 | size: 1024 * 1024, // TODO get from SAFE contants? 402 | version: await this._mData.getVersion() 403 | } 404 | } catch (e) { debug('%s.updateMetadata() - failure: ', this.constructor.name, e.message) } 405 | 406 | this._metadata = metadata 407 | return this._metadata 408 | } 409 | 410 | async getValueVersion (key) { 411 | try { 412 | return this._safeJs.getMutableDataValueVersion(this._mData, key) 413 | } catch (e) { 414 | error(e) 415 | throw e 416 | } 417 | } 418 | 419 | async getEntryValue (key) { 420 | try { 421 | let valueVersion = await this._safeJs.getMutableDataValueVersion(this._mData, key) 422 | // For no entry or deleted entry return undefined 423 | return (valueVersion && valueVersion.buf.byteLength !== 0 ? valueVersion.buf : undefined) 424 | } catch (e) { 425 | debug('%s.getEntryValue(%s) failed', this.constructor.name, key) 426 | throw e 427 | } 428 | } 429 | 430 | /** 431 | * Get the MutableData entry for entryKey 432 | * @param {String} entryKey 433 | * @return {Promise} resolves to ValueVersion 434 | */ 435 | async getEntry (entryKey) { 436 | // TODO uses this._mData rather than NFS emulation with this.nfs() 437 | } 438 | 439 | async getEntryAsFile (key) { 440 | try { 441 | return this.nfs().fetch(key) 442 | } catch (e) { error(e) } 443 | } 444 | 445 | async getEntryAsNfsContainer (key) { 446 | // TODO review value of getEntryAsNfsContainer and use of safeJs.getNfsContainer rather than new NfsContainer 447 | try { 448 | let value = await this.getEntryValue(key) 449 | return this._safeJs.getNfsContainer(value, false, this.isPublic(), this) 450 | } catch (e) { error(e) } 451 | } 452 | 453 | async createNfsFolder (folderPath) { 454 | debug('TODO createNfsFolder(\'%s\')', folderPath) 455 | } 456 | 457 | async insertNfsFolder (nfsFolder) { 458 | debug('TODO insertNfsFolder(\'%s\')', nfsFolder) 459 | } 460 | 461 | // TODO add further helper methods to the above 462 | 463 | // File System Interface 464 | // ===================== 465 | // 466 | // The following methods provide a simplified FS interface as 467 | // a way to access and manipulate containers. You can always 468 | // get the mutable data of a container and use that directly 469 | // instead or as well as this interface. 470 | // 471 | // Any app can interact with a SAFE through this FS style 472 | // interface, not just for access to storage but also 473 | // features such as public names and SAFE services using 474 | // the corresponding container classes. 475 | // 476 | // This interface assumes a heirarchy of containers as follows: 477 | // / 478 | // \-- 479 | // \-- 480 | // : 481 | // \-- 482 | // Where: 483 | // is a default container for files 484 | // such as _public, _music etc 485 | // is a mutable data used to store files 486 | // using the SAFE API emulate as 'NFS' feature. 487 | // 488 | // /_publicNames 489 | // \-- 490 | // \-- 491 | // \-- 492 | // : 493 | // \-- 494 | // 495 | // Where: 496 | // is used to store services on a public name 497 | // depends on the service (e.g. for www it will be 498 | // an ) 499 | // 500 | // The safenetwork-fuse app uses this interface to provide a 501 | // virtual SAFE file system which can be mounted locally but maintains 502 | // an independent heirarchy which allows any container to be 503 | // mounted at an arbitrary path, whereas the above heirarchy is 504 | // fixed. 505 | // 506 | // TODO consider supporting stand-alone containers for arbitrary mutable data as follows: 507 | // /_mutabledata 508 | // \-- 509 | // \-- 510 | // : 511 | // \-- 512 | // Adding a container holder class for /_mutabledata will allow 513 | // the use of the MutableDataContainer class to hold a list of 514 | // MutableDataContainers of arbitrary type. This is intended only 515 | // for *STAND-ALONE* mutable data (i.e. where they are not linked 516 | // together). For linked mutable data (as in the heirarchy) new 517 | // classes should be used to ensure that the cache map is able 518 | // to automatically update the MD of a child if it changes in 519 | // the parent. 520 | 521 | /** 522 | * Internal helpers - the implementations here are templates which 523 | * should be overriden in each container class. 524 | **/ 525 | 526 | /** 527 | * Get portion of the key to use as a relative path within this container 528 | * 529 | * @param {String} itemPath full path to the item (not just relative to this container's 'root') 530 | * @return {String} the part of itemPath that lies within this container's 'mount', or undefined 531 | */ 532 | _getKeyPartOf (itemPath) { 533 | let key 534 | if (itemPath.indexOf(this._containerPath) === 0) key = itemPath.substring(this._containerPath.length) 535 | return key 536 | } 537 | 538 | /** 539 | * Create a container object of the appropriate class to wrap an MD pointed to an entry in this (parent) container 540 | * 541 | * @param {String} key a string matching a mutable data entry in this (parent) container 542 | * @return {Promise} a suitable SAFE container for the entry value (mutable data) 543 | */ 544 | async _createChildContainerForEntry (key) { 545 | let msg = 'ERROR: SafeContainer._createChildContainerForEntry() should be overridden in extending class: ' + this.constructor.name 546 | debug(msg) 547 | throw new Error(msg) 548 | } 549 | 550 | // TODO BUG using 'key' on containerMap is not unique enough because 551 | // could have different NFS containers at the same key in different 552 | // parents (e.g. one in _public, one in _documents). So this 553 | // needs to be based on the full path including parent. 554 | async _getContainerForKey (key) { 555 | debug('%s._getContainerForKey(\'%s\')', this.constructor.name, key) 556 | 557 | let container 558 | try { 559 | container = await containerMap.get(key) 560 | if (!container) { 561 | container = await this._createChildContainerForEntry(key) 562 | if (container) { 563 | containerMap.put(key, container) 564 | await container.initialise() 565 | } 566 | } 567 | } catch (e) { error(e) } 568 | 569 | return container 570 | } 571 | 572 | // FS implementations 573 | 574 | // These methods operate on the data held in a mutable data container class 575 | // so long as the path lies within the container itself. If the path is 576 | // longer, and refers to data held by a child, the child will be called 577 | // to perform the operation after stripping the part of the path 578 | // relating to the parent. So folderPath is always relative to the 579 | // current container. 580 | // 581 | 582 | /** 583 | * Return a copy of Mutable Data Entry with additional decoded members: plainKey and plainValue 584 | * 585 | * This implementation attempts to decrypt keys and entries. 586 | * 587 | * Child classes can implement a simpler version that always or never decrypts, as needed 588 | * 589 | * @param {Object} entry An entry object, such as those returned by MutableData.listEntries() 590 | * @param {Object} options [optional] when omitted, decode key and value, otherwise depends on properties of decodeKey and decodeValue 591 | * @return {Promise} [description] 592 | */ 593 | // TODO delete this safe-node-app v0.8.1 version: 594 | async _decodeEntry081 (key, val, options) { 595 | let entry = {'key': key, 'value': val} 596 | return this._decodeEntry(entry, options) 597 | } 598 | 599 | async _decodeEntry (entry, options) { 600 | let decodedEntry = entry 601 | try { 602 | let enc = new TextDecoder() 603 | 604 | if (!options || options.decodeKey) { 605 | let plainKey = entry.key 606 | try { plainKey = await this._mData.decrypt(plainKey) } catch (e) { console.log('Key decryption ERROR: %s', e) } 607 | plainKey = enc.decode(new Uint8Array(plainKey)) 608 | if (plainKey !== entry.key.toString()) 609 | debugEntry('Key (encrypted): ', entry.key.toString()) 610 | debugEntry('Key : ', plainKey) 611 | decodedEntry.plainKey = plainKey 612 | } 613 | 614 | if (!options || options.decodeValue) { 615 | let plainValue = entry.value.buf 616 | try { plainValue = await this._mData.decrypt(plainValue) } catch (e) { debug('Value decryption ERROR: %s', e) } 617 | plainValue = enc.decode(new Uint8Array(plainValue)) 618 | if (plainValue !== entry.value.buf.toString()) 619 | debugEntry('Value (encrypted): ', entry.value.buf.toString()) 620 | debugEntry('Value :', plainValue) 621 | 622 | debugEntry('Version: ', entry.value.version) 623 | decodedEntry.plainValue = plainValue 624 | } 625 | 626 | return decodedEntry 627 | } catch (e) { error(e) } 628 | } 629 | 630 | /** 631 | * Get listing of folder 632 | * @param {String} folderPath 633 | * 634 | * @return {Promise} Object { status: C.SUCCESS or an Error object 635 | * listing: list of file and folder names (folders end with '/') } 636 | */ 637 | async listFolder (folderPath) { 638 | debug('%s.listFolder(\'%s\')', this.constructor.name, folderPath) 639 | 640 | try { 641 | let result = await this.listFolderResultsRef(folderPath) 642 | if (result.status === C.SUCCESS) { 643 | return { status: C.SUCCESS, listing: result.resultsRef.result } 644 | } 645 | return result 646 | } catch (e) { 647 | error(e) 648 | return { status: e } 649 | } 650 | } 651 | 652 | async listFolderResultsRef (folderPath) { 653 | debug('%s.listFolderResulsRef(\'%s\')', this.constructor.name, folderPath) 654 | 655 | let listing = [] // Result from this container 656 | let resultsRef // Set if result obtained from child container 657 | try { 658 | // TODO remove debug calls (and comment out the value parts until moved elsewhere) 659 | // TODO if a defaultContainer check cache against sub-paths of folderPath (longest first) 660 | // Only need to check container entries if that fails 661 | 662 | // In some cases the name of the container appears at the start 663 | // of the key (e.g. '/_public/happybeing/root-www'). 664 | // In other such as an NFS container it is just the file name 665 | // or possibly a path which could container directory separators 666 | // such as 'index.html' or 'images/profile-picture.png' 667 | 668 | // We add this._subTree to the front of the path 669 | let folderMatch = this._subTree + folderPath 670 | 671 | // For matching we ignore a trailing '/' so remove if present 672 | folderMatch = (u.isNfsFolder(folderMatch) ? folderMatch.substring(0, folderMatch.length - 1) : folderMatch) 673 | 674 | let listingQ = [] 675 | let entries = await this._mData.getEntries() 676 | let entriesList = await entries.listEntries() 677 | entriesList.forEach(async (entry) => { 678 | if (!this.isDeletedEntryValue(entry.value)) { 679 | listingQ.push(new Promise(async (resolve, reject) => { 680 | let decodedEntry = await this._decodeEntry(entry) 681 | let plainKey = decodedEntry.plainKey 682 | 683 | // For matching we ignore a trailing '/' so remove if present 684 | let matchKey = (u.isNfsFolder(plainKey) ? plainKey.substring(0, plainKey.length - 1) : plainKey) 685 | debugEntry('Match Key : ', matchKey) 686 | debugEntry('Folder Match : ', folderMatch) 687 | // Ignore metadata entries 688 | if (this.isHiddenEntry(plainKey)) { 689 | resolve() 690 | return // Skip this one 691 | } 692 | 693 | if (folderMatch === '') { // Check if the folderMatch is root of the key 694 | // Item is the first part of the path 695 | let item = plainKey.split('/')[0] 696 | if (item && item.length && listing.indexOf(item) === -1) { 697 | debugEntry('listing-1.push(\'%s\')', item) 698 | listing.push(item) 699 | } 700 | } else if (plainKey.indexOf(folderMatch) === 0 && plainKey.length > folderMatch.length) { 701 | // As the folderMatch is at the start of the key, and *shorter*, 702 | // item is the first part of the path after the folder (plus a '/') 703 | let item = plainKey.substring(folderMatch.length).split('/')[1] 704 | if (item && item.length && listing.indexOf(item) === -1) { 705 | debugEntry('listing-2.push(\'%s\')', item) 706 | listing.push(item) 707 | } 708 | } else if (folderMatch.indexOf(plainKey) === 0 && 709 | (folderMatch.charAt(plainKey.length) === '/' || 710 | folderMatch === plainKey)) { 711 | // We've matched the key of a child container, so pass to child 712 | let matchedChild = await this._getContainerForKey(plainKey) 713 | let subFolder = folderMatch.substring(plainKey.length) 714 | if (subFolder[0] === '/') subFolder = subFolder.substring(1) 715 | // As it's the child, call listFolderResultsRef() to use its cache 716 | let folderResult = await matchedChild.listFolderResultsRef(subFolder) 717 | if (folderResult.status !== C.SUCCESS) { 718 | throw folderResult.status 719 | } 720 | resultsRef = folderResult.resultsRef 721 | debugEntry('%s.listing-3: %o', constructor.name, resultsRef.resultsMap[resultsRef.resultsKey]['listFolder']) 722 | } 723 | resolve() // Resolve the entry's promise 724 | }).catch((e) => error(e))) 725 | } 726 | }) 727 | await Promise.all(listingQ).catch((e) => error(e)) 728 | debugEntry('%s.listing-4-END: %o', constructor.name, listing) 729 | } catch (e) { 730 | error(e) 731 | debug('ERROR %s.listFolder(\'%s\') failed', this.constructor.name, folderPath) 732 | } 733 | 734 | if (!resultsRef) { 735 | debugEntry('%s.listing-6-END: %o', constructor.name, listing) 736 | resultsRef = this._cacheResultForPath(folderPath, 'listFolder', listing) 737 | } 738 | return { status: C.SUCCESS, 'resultsRef': resultsRef } 739 | } 740 | 741 | /** 742 | * Call functionName on self or child container (if exact match of entry key) 743 | * @private 744 | * 745 | * @param {String} itemPath 746 | * @param {String} functionName 747 | * @param {Unknown} p2 [optional] parameter for functionName 748 | * @param {Unknown} p3 749 | * @param {Unknown} p4 750 | * @param {Unknown} p5 751 | * @return {Promise} 752 | */ 753 | async _callFunctionOnItem (itemPath, functionName, p2, p3, p4, p5) { 754 | debug('%s._callFunctionOnItem(%s, %s, p2, p3, p4, p5)', this.constructor.name, itemPath, functionName) 755 | 756 | let result 757 | try { 758 | // TODO if a defaultContainer check cache against sub-paths of folderPath (longest first) 759 | // Only need to check container entries if that fails 760 | 761 | // In some cases the name of the container appears at the start 762 | // of the key (e.g. '/_public/happybeing/root-www'). 763 | // In other such as an NFS container it is just the file name 764 | // or possibly a path which could container directory separators 765 | // such as 'index.html' or 'images/profile-picture.png' 766 | 767 | // For matching we ignore a trailing '/' so remove if present 768 | let itemMatch = (u.isNfsFolder(itemPath) ? itemPath.substring(0, itemPath.length - 1) : itemPath) 769 | 770 | // We add this._subTree to the front of the path 771 | itemMatch = this._subTree + itemMatch 772 | 773 | // TODO remove debug calls (and comment out the value parts until moved elsewhere) 774 | let entries = await this._mData.getEntries() 775 | // TODO revert to safe-node-app v0.9.1 code: 776 | // let entriesList = await entries.listEntries() 777 | // TODO remove safe-node-app v0.8.1 code: 778 | let entriesList = await entries.listEntries() 779 | let entryQ = [] 780 | entriesList.forEach(async (entry) => { 781 | if (!this.isDeletedEntryValue(entry.value)) { 782 | entryQ.push(new Promise(async (resolve, reject) => { 783 | if (!result) { 784 | let decodedEntry = await this._decodeEntry(entry) 785 | let plainKey = decodedEntry.plainKey 786 | 787 | // let plainValue = entry.value.buf 788 | // try { plainValue = await this._mData.decrypt(entry.value.buf) } catch (e) { debug('Value decryption ERROR: %s', e) } 789 | // 790 | // let enc = new TextDecoder() 791 | // let plainKey = enc.decode(new Uint8Array(entry.key)) 792 | // if (plainKey !== entry.key.toString()) 793 | // debugEntry('Key (encrypted): ', entry.key.toString()) 794 | 795 | // For matching we ignore a trailing '/' so remove if present 796 | let matchKey = (u.isNfsFolder(plainKey) ? plainKey.substring(0, plainKey.length - 1) : plainKey) 797 | debugEntry('Key : ', plainKey) 798 | debugEntry('Match Key : ', matchKey) 799 | 800 | // plainValue = enc.decode(new Uint8Array(plainValue)) 801 | // if (plainValue !== entry.value.buf.toString()) 802 | // debugEntry('Value (encrypted): ', entry.value.buf.toString()) 803 | // 804 | // debugEntry('Value :', plainValue) 805 | // 806 | // debugEntry('Version: ', entry.value.version) 807 | 808 | if (!this.isHiddenEntry(plainKey)) { 809 | // Check it the itemMatch is at the start of the key, and *shorter* 810 | if (plainKey.indexOf(itemMatch) === 0 && plainKey.length > itemMatch.length) { 811 | // Item is the first part of the path after the folder (plus a '/') 812 | let item = plainKey.substring(itemMatch.length + 1).split('/')[1] 813 | result = this[functionName](item, p2, p3, p4, p5) 814 | debugEntry('loop result-1: %o', await result) 815 | resolve(result) 816 | } else if (itemMatch.indexOf(plainKey) === 0) { 817 | // We've matched the key of a child container, so pass to child 818 | let matchedChild = await this._getContainerForKey(plainKey) 819 | debug('calling matchedChild %s.%s(%s,...)', matchedChild.constructor.name, functionName, itemMatch.substring(plainKey.length + 1)) 820 | result = await matchedChild[functionName](itemMatch.substring(plainKey.length + 1), p2, p3, p4, p5) 821 | debugEntry('loop result-2: %o', result) 822 | resolve(result) 823 | } 824 | } 825 | } 826 | resolve(undefined) 827 | }).catch((e) => error(e))) 828 | } 829 | }) 830 | await Promise.all(entryQ).catch((e) => error(e)) 831 | if (result === undefined) { 832 | debug('WARNING: %s._callFunctionOnItem(%s, %s) - item not found to call', this.constructor.name, itemPath, functionName) 833 | result = containerTypeCodes.notFound 834 | } 835 | debug('%s.call returning result: %o', constructor.name, result) 836 | return result 837 | } catch (e) { 838 | debug('ERROR: %s._callFunctionOnItem(%s, %s) failed', this.constructor.name, itemPath, functionName) 839 | error(e) 840 | } 841 | } 842 | 843 | async itemInfo (itemPath) { 844 | debug('%s.itemInfo(\'%s\')', this.constructor.name, itemPath) 845 | try { 846 | if (this.isSelf(itemPath)) { 847 | return this._safeJs.mutableDataStats(this._mData) 848 | } else if (this.itemType(itemPath) === containerTypeCodes.fakeContainer) { 849 | return { 850 | // TODO consider using listFolder to count folders, recursing, and then adding info from NFS containers 851 | // TODO these members are junk (inherited from IPFS code so change them!) 852 | repoSize: 12345, 853 | storageMax: 99999, 854 | numObjects: 321 855 | } 856 | } else { 857 | // Pass to the child container 858 | return this._callFunctionOnItem(itemPath, 'itemInfo') 859 | } 860 | } catch (e) { error(e) } 861 | } 862 | 863 | /** 864 | * Get the type of the item as one of containerTypeCodes values 865 | * 866 | * @param {String} itemPath A partial or full key within the scope of the container 867 | * @return {String} A containerTypeCodes value 868 | */ 869 | 870 | _entryTypeOf (key) { 871 | // This caters for default folders, except _publicNames 872 | return containerTypeCodes.nfsContainer 873 | } 874 | 875 | async itemType (itemPath) { 876 | debug('%s.itemType(\'%s\')', this.constructor.name, itemPath) 877 | let type = containerTypeCodes.notValid 878 | try { 879 | let itemKey = this._subTree + itemPath 880 | let value = await this.getEntryValue(itemKey) 881 | if (value) { // itemPath exact match with entry key, so determine entry type for this container 882 | type = this._entryTypeOf(itemKey) 883 | } else if (this.isSelf(itemPath)) { 884 | type = containerTypeCodes.defaultContainer 885 | } else { 886 | // Check for fakeContainer or NFS container 887 | let itemAsFolder = (u.isNfsFolder(itemPath) ? itemPath : itemPath + '/') 888 | let shortestEnclosingKey = await this._getShortestEnclosingKey(itemAsFolder) 889 | if (shortestEnclosingKey) { 890 | if (shortestEnclosingKey.length !== itemPath.length) { 891 | type = containerTypeCodes.fakeContainer 892 | } else { 893 | type = containerTypeCodes.nfsContainer 894 | } 895 | } else { 896 | type = containerTypeCodes.childContainerItem 897 | // TODO delete old code: 898 | // WAS:// Attempt to call itemType on a child container 899 | // type = await this._callFunctionOnItem(itemPath, 'itemType') 900 | } 901 | } 902 | } catch (e) { 903 | debug('file not found') 904 | error(e) 905 | throw e 906 | } 907 | debug('itemType(%s) returning: ', itemPath, type) 908 | return type 909 | } 910 | 911 | // Get the shortest key where itemPath is part of the key 912 | // TODO this is probably horribly inefficient 913 | async _getShortestEnclosingKey (itemPath) { 914 | debug('%s._getShortestEnclosingKey(\'%s\')', this.constructor.name, itemPath) 915 | 916 | // We add this._subTree to the front of the path 917 | let itemMatch = this._subTree + itemPath 918 | debugEntry('Matching path: ', itemMatch) 919 | 920 | let result 921 | let resultQ = [] 922 | try { 923 | let entries = await this._mData.getEntries() 924 | let entriesList = await entries.listEntries() 925 | entriesList.forEach(async (entry) => { 926 | if (!this.isDeletedEntryValue(entry.value)) { 927 | resultQ.push(new Promise(async (resolve, reject) => { 928 | let decodedEntry = await this._decodeEntry(entry, {decodeKey: true}) 929 | let plainKey = decodedEntry.plainKey 930 | debugEntry('Key : ', plainKey) 931 | debugEntry('Matching against: ', itemMatch) 932 | 933 | if (plainKey.indexOf(itemMatch) === 0) { 934 | if (!result || result.length > plainKey.length) { 935 | result = plainKey 936 | debugEntry('MATCHED: ', plainKey) 937 | } 938 | } 939 | resolve() 940 | }).catch((e) => error(e))) 941 | } 942 | }) 943 | return Promise.all(resultQ).then(_ => { 944 | debug('MATCH RESULT: ', (result !== undefined ? result : '')) 945 | return result 946 | }).catch((e) => error(e)) 947 | } catch (e) { error(e) } 948 | } 949 | 950 | /** 951 | * Get attributes of a file or directory 952 | * @param {String} itemPath 953 | * @param {Number} fd [optional] file descriptor (if file is open) 954 | * 955 | * @return {Promise} Object { status: C.SUCCESS or an Error object 956 | * attributes: an attibutes object } 957 | */ 958 | async itemAttributes (itemPath, fd) { 959 | debug('%s.itemAttributes(\'%s\', %s)', this.constructor.name, itemPath, fd) 960 | 961 | try { 962 | let result = await this.itemAttributesResultsRef(itemPath, fd) 963 | if (result.status === C.SUCCESS) { 964 | return { status: C.SUCCESS, attributes: result.resultsRef.result } 965 | } 966 | return result 967 | } catch (e) { 968 | error(e) 969 | return { status: e } 970 | } 971 | } 972 | 973 | /* @return {Promise} Object { status: C.SUCCESS or an Error object 974 | * resultsRef: Object } 975 | */ 976 | async itemAttributesResultsRef (itemPath, fd) { 977 | debug('%s.itemAttributesResultsRef(\'%s\')', this.constructor.name, itemPath) 978 | let fileOperation = 'itemAttributes' 979 | 980 | const now = Date.now() 981 | let result 982 | let resultsRef // Will be set if result is from child (and so cached by child) 983 | try { 984 | if (this.isSelf(itemPath)) { 985 | debug('%s is type: %s', itemPath, containerTypeCodes.defaultContainer) 986 | await this.updateMetadata() 987 | result = { 988 | // TODO improve this if SAFE accounts ever have suitable values for size etc: 989 | modified: now, 990 | accessed: now, 991 | created: now, 992 | size: this._metadata.size, 993 | version: this._metadata.version, 994 | 'isFile': false, 995 | entryType: containerTypeCodes.defaultContainer 996 | } 997 | } 998 | 999 | let type 1000 | if (!result) { 1001 | type = await this.itemType(itemPath) 1002 | if (type === containerTypeCodes.file || 1003 | type === containerTypeCodes.nfsContainer || 1004 | type === containerTypeCodes.service || 1005 | type === containerTypeCodes.servicesContainer || 1006 | type === containerTypeCodes.childContainerItem) { 1007 | debug('%s is type: %s', itemPath, type) 1008 | let callResult = await this._callFunctionOnItem(itemPath, 'itemAttributesResultsRef') 1009 | if (callResult.status === C.SUCCESS) resultsRef = callResult.resultsRef 1010 | } 1011 | } 1012 | 1013 | if (!result && !resultsRef) { 1014 | if (type === containerTypeCodes.fakeContainer) { 1015 | // Fake container 1016 | debug('%s is type: %s', itemPath, containerTypeCodes.fakeContainer) 1017 | // Default values (used as is for containerTypeCodes.nfsContainer) 1018 | result = { 1019 | modified: now, 1020 | accessed: now, 1021 | created: now, 1022 | size: 0, 1023 | version: -1, 1024 | 'isFile': false, 1025 | entryType: type 1026 | } 1027 | } else { 1028 | result = { entryType: type } 1029 | } 1030 | } 1031 | } catch (e) { 1032 | error(e) 1033 | return { status: e } 1034 | } 1035 | if (!resultsRef) { 1036 | resultsRef = this._cacheResultForPath(itemPath, fileOperation, result) 1037 | } 1038 | 1039 | return { status: C.SUCCESS, 'resultsRef': resultsRef } 1040 | } 1041 | 1042 | async openFile (itemPath, nfsFlags) { 1043 | debug('%s.openFile(\'%s\', %s)', this.constructor.name, itemPath, nfsFlags) 1044 | try { 1045 | // Default is a container of containers, not files so pass to child container 1046 | return await this._callFunctionOnItem(itemPath, 'openFile', nfsFlags) 1047 | } catch (e) { 1048 | error(e) 1049 | return { status: e } 1050 | } 1051 | } 1052 | 1053 | async createFile (itemPath) { 1054 | debug('%s.createFile(\'%s\')', this.constructor.name, itemPath) 1055 | try { 1056 | // Default is a container of containers, not files so pass to child container 1057 | return await this._callFunctionOnItem(itemPath, 'createFile') 1058 | } catch (e) { 1059 | error(e) 1060 | return { status: e } 1061 | } 1062 | } 1063 | 1064 | async closeFile (itemPath, fd) { 1065 | debug('%s.closeFile(\'%s\', %s)', this.constructor.name, itemPath, fd) 1066 | try { 1067 | // Default is a container of containers, not files so pass to child container 1068 | return await this._callFunctionOnItem(itemPath, 'closeFile', fd) 1069 | } catch (e) { 1070 | error(e) 1071 | return { status: e } 1072 | } 1073 | } 1074 | 1075 | /** 1076 | * delete a file 1077 | * 1078 | * @param {String} itemPath 1079 | * @return {Promise} Object { result: true on success, 1080 | * wasLastItem: true if itemPath folder left emtpy } 1081 | */ 1082 | async deleteFile (itemPath) { 1083 | debug('%s.deleteFile(\'%s\')', this.constructor.name, itemPath) 1084 | try { 1085 | // Default is a container of containers, not files so pass to child container 1086 | return await this._callFunctionOnItem(itemPath, 'deleteFile') 1087 | } catch (e) { 1088 | error(e) 1089 | return { status: e } 1090 | } 1091 | } 1092 | 1093 | /** 1094 | * rename a file and/or move between paths within this container 1095 | * 1096 | * @param {String} itemPath 1097 | * @param {String} newItemPath 1098 | * @return {Promise} Object { status: true on success, 1099 | * wasLastItem: true if itemPath folder left emtpy } 1100 | */ 1101 | async moveFile (itemPath, newItemPath) { 1102 | debug('%s.moveFile(\'%s\', \'%s\')', this.constructor.name, itemPath, newItemPath) 1103 | try { 1104 | // Default is a container of containers, not files so pass to child container 1105 | return await this._callFunctionOnItem(itemPath, 'moveFile', newItemPath) 1106 | } catch (e) { 1107 | error(e) 1108 | return { status: e } 1109 | } 1110 | } 1111 | 1112 | /** 1113 | * Get user metadata for a file (file does not need to be open) 1114 | * @param {String} itemPath 1115 | * @param {Number} fd [optional] file descriptor obtained from openFile() or createFile() 1116 | * @return {Promise} A buffer containing any metadata as previously set 1117 | */ 1118 | async getFileMetadata (itemPath, fd) { 1119 | try { 1120 | // Default is a container of containers, not files so pass to child container 1121 | return await this._callFunctionOnItem(itemPath, 'getFileMetadata', fd) 1122 | } catch (e) { 1123 | error(e) 1124 | return { status: e } 1125 | } 1126 | } 1127 | 1128 | /** 1129 | * Set metadata to be written when on closeFile() (for a file opened for write) 1130 | * 1131 | * Note: must only be called after succesful createFile() or openFile() for write 1132 | * @param {String} itemPath 1133 | * @param {Number} fd [optional] file descriptor 1134 | * @param {Buffer} metadata Metadata that will be written on closeFile() 1135 | */ 1136 | async setFileMetadata (itemPath, fd, metadata) { 1137 | try { 1138 | // Default is a container of containers, not files so pass to child container 1139 | return await this._callFunctionOnItem(itemPath, 'setFileMetadata', fd, metadata) 1140 | } catch (e) { 1141 | error(e) 1142 | return { status: e } 1143 | } 1144 | } 1145 | 1146 | // Read up to len bytes starting from pos 1147 | // return as a String 1148 | async readFile (itemPath, fd, pos, len) { 1149 | debug('%s.readFile(\'%s\', %o, %s, %s)', this.constructor.name, itemPath, fd, pos, len) 1150 | try { 1151 | // Default is a container of containers, not files so pass to child container 1152 | return await this._callFunctionOnItem(itemPath, 'readFile', fd, pos, len) 1153 | } catch (e) { 1154 | error(e) 1155 | return { status: e } 1156 | } 1157 | } 1158 | 1159 | // Write up to len bytes into buf (Uint8Array), starting at pos 1160 | // return number of bytes written 1161 | async readFileBuf (itemPath, fd, buf, pos, len) { 1162 | debug('%s.readFileBuf(\'%s\', %o, buf, %s, %s)', this.constructor.name, itemPath, fd, pos, len) 1163 | try { 1164 | // Default is a container of containers, not files so pass to child container 1165 | return await this._callFunctionOnItem(itemPath, 'readFileBuf', fd, buf, pos, len) 1166 | } catch (e) { 1167 | error(e) 1168 | return { status: e } 1169 | } 1170 | } 1171 | 1172 | /** 1173 | * Write up to len bytes starting from pos 1174 | * 1175 | * This function can be used in one of two ways: 1176 | * - simple: just call writeFile() and it will write data, and if the 1177 | * file is not open yet, it will do that first 1178 | * - you can call openFile() before, to open in a specific mode using flags 1179 | * 1180 | * Note: if this function fails, the cached file state is purged and any file 1181 | * descriptor will be invalidated 1182 | * 1183 | * @param {String} itemPath path (key) of the file (in container which owns this _NfsContainerFiles) 1184 | * @param {Number} fd [optional] file descriptor obtained from openFile() 1185 | * @param {Buffer|String} content (Number | C.NFS_FILE_END) 1186 | * @return {Promise} String container bytes read 1187 | */ 1188 | async writeFile (itemPath, fd, content) { 1189 | debug('%s.writeFile(\'%s\', %s, ...)', this.constructor.name, itemPath, fd) 1190 | try { 1191 | // Default is a container of containers, not files so pass to child container 1192 | return await this._callFunctionOnItem(itemPath, 'writeFile', fd, content) 1193 | } catch (e) { 1194 | error(e) 1195 | return { status: e } 1196 | } 1197 | } 1198 | 1199 | /** 1200 | * Write to file, len bytes from buf (Uint8Array) 1201 | * 1202 | * This function can be used in one of two ways: 1203 | * - simple: just call writeFileBuf() and it will write data, and if the 1204 | * file is not open yet, it will do that first 1205 | * - you can call openFile() before, to open in a specific mode using flags 1206 | * 1207 | * Note: if this function fails, the cached file state is purged and any file 1208 | * descriptor will be invalidated 1209 | * 1210 | * @param {String} itemPath path (key) of the file (in container which owns this _NfsContainerFiles) 1211 | * @param {Number} fd [optional] file descriptor obtained from openFile() 1212 | * @param {Uint8Array} buf [description] 1213 | * @param {Number} len 1214 | * @param {Number} pos [optional] position of file to write (must not be less than end of last write) 1215 | * @return {Promise} Number of bytes written to file 1216 | */ 1217 | async writeFileBuf (itemPath, fd, buf, len, pos) { 1218 | debug('%s.writeFileBuf(\'%s\', %s, buf, %s, %s)', this.constructor.name, itemPath, fd, len, pos) 1219 | 1220 | try { 1221 | // Default is a container of containers, not files so pass to child container 1222 | return await this._callFunctionOnItem(itemPath, 'writeFileBuf', fd, buf, len, pos) 1223 | } catch (e) { 1224 | error(e) 1225 | return { status: e } 1226 | } 1227 | } 1228 | 1229 | /** 1230 | * Truncate a file to size bytes (only implements size === 0) 1231 | * 1232 | * @private This function is implemented purely to allow FUSE to open a 1233 | * file for append, but overwrite it by first truncating its size to zero. 1234 | * This is needed because POSIX open() only has flags for write, not for 1235 | * append. But since SAFE NFS lacks file truncate, we can only truncate 1236 | * to zero which we do be creating a new file with NFS open(). 1237 | * 1238 | * When opening a SAFE NFS file for write we must 'append', otherwise FUSE 1239 | * would have now way to append (since it can only open() for write, not 1240 | * write with append). In turn, the ony way to allow FUSE to be able to open 1241 | * and overwrite an existing NFS file is to implement truncate at size zero. 1242 | * 1243 | * @param {String} itemPath 1244 | * @param {Number} fd 1245 | * @param {Number} size 1246 | * @return {Promise} 1247 | */ 1248 | async _truncateFile (itemPath, fd, size) { 1249 | debug('%s._truncateFile(\'%s\', %s, %s)', this.constructor.name, itemPath, fd, size) 1250 | try { 1251 | if (size !== 0) throw new Error('truncateFile() not implemented for size other than zero') 1252 | // Default is a container of containers, not files so pass to child container 1253 | return await this._callFunctionOnItem(itemPath, '_truncateFile', fd, size) 1254 | } catch (e) { 1255 | error(e) 1256 | return e 1257 | } 1258 | } 1259 | 1260 | /** File system operation results cache 1261 | 1262 | These functions are called by the container's _NfsContainerFiles object 1263 | rather than by the container itself, because that implements the file 1264 | operations. 1265 | */ 1266 | 1267 | _handleCacheForCreateFileOrOpenWrite (itemPath) { 1268 | // File creation sets up itemAttributes for the new file, so leave that in place 1269 | // Only need to handle listFolder here: 1270 | this._clearResultForPath(u.parentPathNoDot(itemPath), 'listFolder') 1271 | } 1272 | 1273 | /** 1274 | * update cache when an item has been deleted 1275 | * 1276 | * @param {String} itemPath 1277 | * @return {Promise} true, if the item was the last in its parent folder 1278 | */ 1279 | async _handleCacheForDelete (itemPath) { 1280 | let wasLastItem = await this._handleCacheListFolderRemoveItem(itemPath) 1281 | this._clearResultForPath(itemPath, '*') 1282 | return wasLastItem 1283 | } 1284 | 1285 | async _handleCacheForRename (itemPath, newItemPath) { 1286 | // Handle create before delete, otherwise the handle delete may wrongly 1287 | // act on directory becoming empty 1288 | this._handleCacheForCreateFileOrOpenWrite(newItemPath) 1289 | await this._handleCacheForDelete(itemPath) 1290 | } 1291 | 1292 | async _handleCacheListFolderAddItem (itemPath) { 1293 | let folderPath = u.parentPathNoDot(itemPath) 1294 | let itemName = u.itemPathBasename(itemPath) 1295 | let thing; thing.bang() // TODO IS THIS USED? 1296 | 1297 | try { 1298 | let listFolderResult 1299 | let resultsHolder = this._resultHolderMap[folderPath] 1300 | if (resultsHolder) listFolderResult = resultsHolder['listFolder'] 1301 | if (!listFolderResult) { 1302 | let resultsRef = await this.listFolderResultsRef(folderPath) 1303 | listFolderResult = resultsRef.result 1304 | } 1305 | if (listFolderResult.indexOf(itemName) === -1) listFolderResult.push(itemName) 1306 | } catch (e) { error(e) } 1307 | } 1308 | 1309 | /** 1310 | * update cache when an item has been deleted 1311 | * 1312 | * @param {String} itemPath 1313 | * @return {Promise} true, if the item was the last in its parent folder 1314 | */ 1315 | async _handleCacheListFolderRemoveItem (itemPath) { 1316 | let wasLastItem = true 1317 | try { 1318 | let folderPath = u.parentPathNoDot(itemPath) 1319 | let itemName = u.itemPathBasename(itemPath) 1320 | 1321 | let listFolderResult 1322 | let resultsHolder = this._resultHolderMap[folderPath] 1323 | if (resultsHolder) listFolderResult = resultsHolder['listFolder'] 1324 | if (!listFolderResult) { 1325 | let result = await this.listFolderResultsRef(folderPath) 1326 | if (result.status !== C.SUCCESS) throw result.status 1327 | listFolderResult = result.resultsRef.result 1328 | } 1329 | 1330 | // Remove itemName from the result 1331 | var itemIndex = listFolderResult.indexOf(itemName) 1332 | if (itemIndex > -1) { 1333 | listFolderResult.splice(itemIndex, 1) 1334 | } 1335 | wasLastItem = listFolderResult.length === 0 1336 | if (wasLastItem) { 1337 | // When a folder becomes empty, it disappears and that may 1338 | // cause its parent to become empty, and so on. So as a 1339 | // precaution, we clear the cache for it and all parent folders 1340 | this._clearCacheAlongWholePath(folderPath) 1341 | } 1342 | } catch (e) { error(e) } 1343 | 1344 | return wasLastItem 1345 | } 1346 | 1347 | _handleCacheForChangedAttributes (itemPath) { 1348 | this._clearResultForPath(itemPath, 'itemAttributes') 1349 | } 1350 | 1351 | /** 1352 | * clear cached result for fileOperation(s) at a given path 1353 | * @private 1354 | * 1355 | * @param {String} itemPath 1356 | * @param {String} name of operation (e.g. 'itemAttributes') or '*' to clear all 1357 | */ 1358 | _clearResultForPath (itemPath, fileOperation) { 1359 | debug('%s._clearResultForPath(%s, %s)', this.constructor.name, itemPath, fileOperation) 1360 | let resultsHolder = this._resultHolderMap[itemPath] 1361 | if (resultsHolder) { 1362 | if (fileOperation === '*') { 1363 | let operations = Object.keys(resultsHolder) 1364 | for (let i = 0, len = operations.length; i < len; i++) { 1365 | delete resultsHolder[operations[i]] 1366 | } 1367 | } else if (resultsHolder[fileOperation]) { 1368 | delete resultsHolder[fileOperation] 1369 | } 1370 | } 1371 | } 1372 | 1373 | _clearCacheAlongWholePath (itemPath) { 1374 | let nextDir = itemPath 1375 | while (nextDir !== '') { 1376 | this._clearResultForPath(nextDir, '*') 1377 | nextDir = u.parentPathNoDot(nextDir) 1378 | } 1379 | } 1380 | 1381 | _getResultHolderForPath (itemPath) { 1382 | let resultHolder = this._resultHolderMap[itemPath] 1383 | if (!resultHolder) { 1384 | resultHolder = {} 1385 | this._resultHolderMap[itemPath] = resultHolder 1386 | } 1387 | return resultHolder 1388 | } 1389 | 1390 | // Used when storing a result cached in this container 1391 | _getResultsRefForPath (itemPath) { 1392 | let resultsRef = this._resultsRefMap[itemPath] 1393 | if (!resultsRef) { 1394 | resultsRef = { 1395 | resultsMap: this._resultHolderMap, 1396 | resultsKey: itemPath 1397 | } 1398 | this._resultsRefMap[itemPath] = resultsRef 1399 | } 1400 | return resultsRef 1401 | } 1402 | 1403 | // Used when storing result cached elsewhere (e.g. in child container) 1404 | _setResultsRefForPath (itemPath, resultsRef) { 1405 | this._resultsRefMap[itemPath] = resultsRef 1406 | } 1407 | 1408 | /** 1409 | * Update cache result/clear cache result, and return a ResultsRef object 1410 | * 1411 | * Creates or updates ResultHolder, _resultHolderMap, _resultsRefMap 1412 | * 1413 | * NOTE: changes here need to be reflected in safetwork-fuse RootContainer 1414 | * TODO better to change RootContainer so it extends SafeContainer 1415 | * 1416 | * @param {String} itemPath 1417 | * @param {String} fileOperation 1418 | * @param {Object} operationResult 1419 | * @param {Boolean} cacheTheResult [optional] if false, clears cache rather than updates 1420 | * @return {Object} A 'resultsRef' which has the result, its cache location 1421 | */ 1422 | _cacheResultForPath (itemPath, fileOperation, operationResult, cacheTheResult) { 1423 | debug('%s._cacheResultForPath(%s, %s, %o, %o)', this.constructor.name, itemPath, fileOperation, operationResult, cacheTheResult) 1424 | if (cacheTheResult === undefined) cacheTheResult = true 1425 | 1426 | // Caller wants it cached, but also check if it is cacheable 1427 | if (cacheTheResult && isCacheableResult(fileOperation, operationResult)) { 1428 | let resultHolder = this._getResultHolderForPath(itemPath) 1429 | resultHolder[fileOperation] = operationResult 1430 | } else { 1431 | this._clearResultForPath(itemPath, fileOperation) 1432 | } 1433 | 1434 | this._debugListCache() 1435 | 1436 | let resultsRef = this._getResultsRefForPath(itemPath) 1437 | resultsRef.result = operationResult // Needed because not all results are cached 1438 | return resultsRef 1439 | } 1440 | 1441 | _debugListCache () { 1442 | if (!process.env.DEBUG) return 1443 | 1444 | debugCache('%s._debugListCache()...', this.constructor.name) 1445 | let keys = Object.keys(this._resultsRefMap) 1446 | for (let i = 0, len = keys.length; i < len; i++) { 1447 | debugCache('%s: %o', keys[i], (this._resultsRefMap[keys[i]].resultsMap)[this._resultsRefMap[keys[i]].resultsKey]) 1448 | } 1449 | } 1450 | } 1451 | 1452 | /** 1453 | * Wrapper for _public (SAFE default container) 1454 | * 1455 | * @extends SafeContainer 1456 | */ 1457 | class PublicContainer extends SafeContainer { 1458 | /** 1459 | * @param {Object} safeJs SafenetworkApi (owner) 1460 | */ 1461 | constructor (safeJs, containerName) { 1462 | if (defaultContainers[containerName] !== PublicContainer) { 1463 | throw new Error('Invalid PrivateContainer name:' + containerName) 1464 | } 1465 | let containerPath = '/' + containerName 1466 | let subTree = containerName + '/' 1467 | super(safeJs, containerName, containerPath, subTree) 1468 | } 1469 | 1470 | /** 1471 | * Create a container object of the appropriate class to wrap an MD pointed to an entry in this (parent) container 1472 | * 1473 | * @param {String} key a string matching a mutable data entry in this (parent) container 1474 | * @return {Promise} a suitable SAFE container for the entry value (mutable data) 1475 | */ 1476 | async _createChildContainerForEntry (key) { 1477 | debug('%s._createChildContainerForEntry(\'%s\') ', this.constructor.name, key) 1478 | let containerPath = key 1479 | return new NfsContainer(this._safeJs, key, containerPath, this, true) 1480 | } 1481 | } 1482 | 1483 | /** 1484 | * Wrapper for private default container such as '_documents', '_music' 1485 | * 1486 | * TODO implement support for private containers (_documents, _music etc) 1487 | * TODO implement support for application own container 1488 | */ 1489 | class PrivateContainer extends SafeContainer { 1490 | /** 1491 | * @param {Object} safeJs SafenetworkApi (owner) 1492 | * @param {String} containerName Name of a private default container such as '_documents' 1493 | */ 1494 | constructor (safeJs, containerName) { 1495 | if (defaultContainers[containerName] !== PrivateContainer) { 1496 | throw new Error('Invalid PrivateContainer name:' + containerName) 1497 | } 1498 | let containerPath = '/' + containerName 1499 | let subTree = containerName + '/' 1500 | super(safeJs, containerName, containerPath, subTree) 1501 | // ??? throw new Error('TODO Implement PrivateContainer class (or switch to using PublicContainer?)') 1502 | } 1503 | 1504 | isPublic () { return false } 1505 | 1506 | /** 1507 | * Create a container object of the appropriate class to wrap an MD pointed to an entry in this (parent) container 1508 | * 1509 | * TODO review security & privacy, consider extra encryption (optional?). 1510 | * see discussion https://safenetforum.org/t/apps-and-access-control/26023/53?u=happybeing 1511 | * 1512 | * @param {String} key a string matching a mutable data entry in this (parent) container 1513 | * @return {Promise} a suitable SAFE container for the entry value (mutable data) 1514 | */ 1515 | async _createChildContainerForEntry (key) { 1516 | debug('%s._createChildContainerForEntry(\'%s\') ', this.constructor.name, key) 1517 | let containerPath = key 1518 | return new NfsContainer(this._safeJs, key, containerPath, this, true) 1519 | } 1520 | } 1521 | 1522 | /** 1523 | * Simplified access to the _publicNames container, including a file system API 1524 | * @extends SafeContainer 1525 | * 1526 | * Refs: 1527 | * https://github.com/maidsafe/rfcs/blob/master/text/0046-new-auth-flow/containers.md 1528 | */ 1529 | class PublicNamesContainer extends SafeContainer { 1530 | /** 1531 | * @param {Object} safeJs SafenetworkApi (owner) 1532 | */ 1533 | constructor (safeJs) { 1534 | let containerName = '_publicNames' 1535 | let containerPath = '/' + containerName 1536 | let subTree = '' 1537 | super(safeJs, containerName, containerPath, subTree) 1538 | if (defaultContainers[this._name] !== PublicNamesContainer) { 1539 | throw new Error('Invalid PublicNamesContainer name:' + containerName) 1540 | } 1541 | } 1542 | 1543 | _entryTypeOf (key) { 1544 | return containerTypeCodes.servicesContainer 1545 | } 1546 | 1547 | // Containers should sanity check keys in case of corruption 1548 | // but still cope with a valid key that has an invalid value 1549 | // 1550 | isValidKey (key) { 1551 | return this._safeJs.isValidPublicName(key) 1552 | } 1553 | 1554 | isHiddenKey (key) { return super.isHiddenKey(key) || !this.isValidKey(key) } 1555 | 1556 | async itemType (itemPath) { 1557 | debug('%s.itemType(\'%s\')', this.constructor.name, itemPath) 1558 | let type = containerTypeCodes.notValid 1559 | try { 1560 | let itemKey = this._subTree + itemPath 1561 | let value = await this.getEntryValue(itemKey) 1562 | if (value) { 1563 | // itemPath exact match with entry key, so determine entry type from the key 1564 | type = this._entryTypeOf(itemKey) 1565 | } else { 1566 | type = containerTypeCodes.childContainerItem 1567 | // TODO delete old code: 1568 | // WAS:// Attempt to call itemType on a child container 1569 | // type = await this._callFunctionOnItem(itemPath, 'itemType') 1570 | } 1571 | } catch (e) { 1572 | type = containerTypeCodes.notValid 1573 | debug('public name not found: ', itemPath) 1574 | error(e) 1575 | } 1576 | 1577 | return type 1578 | } 1579 | 1580 | /** 1581 | * Create a container object of the appropriate class to wrap an MD pointed to an entry in this (parent) container 1582 | * 1583 | * @param {String} key a string matching a mutable data entry in this (parent) container 1584 | * @return {Promise} a suitable SAFE container for the entry value (mutable data) 1585 | */ 1586 | async _createChildContainerForEntry (key) { 1587 | debug('%s._createChildContainerForEntry(\'%s\') ', this.constructor.name, key) 1588 | return new ServicesContainer(this._safeJs, this, key) 1589 | } 1590 | } 1591 | 1592 | /** 1593 | * Wrapper for MutableData services container (for a public name) 1594 | */ 1595 | // TODO extends SafeContainer or something else? 1596 | class ServicesContainer extends SafeContainer { 1597 | /** 1598 | * [constructor description] 1599 | * @param {SafenetworkJs} safeJs SafenetworkJS API object 1600 | * @param {Object} parent (required) the PublicNamesContainer object 1601 | * @param {String} parentEntryKey the key in _publicNames where this MD is/will be stored 1602 | */ 1603 | constructor (safeJs, parent, parentEntryKey) { 1604 | if (!parent || parent.constructor.name !== 'PublicNamesContainer') throw new Error('ServicesContainer must have a parent PublicNamesContainer') 1605 | 1606 | let subTree = '' 1607 | super(safeJs, 'services', parentEntryKey, subTree, parent, parentEntryKey, safeJs.SN_TAGTYPE_SERVICES) 1608 | this._mdName = safeJs.makeServicesMdName(parentEntryKey) 1609 | this._isPublic = true 1610 | } 1611 | 1612 | isDefaultContainer () { return false } 1613 | isPublic () { return this._isPublic } 1614 | 1615 | /** 1616 | * Initialise by accessing existing MutableData compatible with NFS emulation 1617 | * @return {Promise} 1618 | */ 1619 | async initialiseExisting () { 1620 | try { 1621 | if (this._parent) { 1622 | let valueVersion = await this._parent.getEntry(this._parentEntryKey) 1623 | this._mdName = valueVersion.value 1624 | } 1625 | 1626 | this._mData = await this._safeJs.mutableData.newPublic(this._mdName, this._tagType) 1627 | this._mdVersion = await this._mDate.getVersion() // This verifies it exists 1628 | } catch (err) { 1629 | let info = (this._parent ? this._parent.name + '/' + this._parentEntryKey : this._mdName) 1630 | debug('%s failed to init existing MD for ', this.constructor.name, info) 1631 | debug(err.message) 1632 | } 1633 | } 1634 | 1635 | async _createChildContainerForEntry (key) { 1636 | debug('%s._createChildContainerForEntry(\'%s\') ', this.constructor.name, key) 1637 | let containerPath = this._parentEntryKey + '/' + key 1638 | return new NfsContainer(this._safeJs, key, containerPath, this, true) 1639 | } 1640 | 1641 | /** 1642 | * Create a services MutableData and insert into parent PublicNamesContainer 1643 | * @return {Promise} a newly created {MutableData} 1644 | */ 1645 | async createPublicName () { 1646 | this._mData = await this._safeJs.createPublicName(this._parentEntryKey) 1647 | this._mdVersion = this._mData.getVersion() 1648 | return this._mData 1649 | } 1650 | 1651 | // TODO add support for creating services when resuming support for services 1652 | // in safeJs (see setupServiceForHost) 1653 | 1654 | // TODO add methods to access existing service entries (e.g. enumerate, get by name) 1655 | 1656 | // Containers should sanity check keys in case of corruption 1657 | // but still cope with a valid key that has an invalid value 1658 | // 1659 | isValidKey (key) { 1660 | let decodedKey = this._safeJs.decodeServiceKey(key) 1661 | 1662 | return this._safeJs.isValidSubdomain(decodedKey.hostProfile) && 1663 | this._safeJs.isValidServiceId(decodedKey.serviceId) 1664 | } 1665 | 1666 | isValidServiceName (name) { return this._safeJs.isValidServiceName(name) } 1667 | 1668 | isHiddenKey (key) { 1669 | return super.isHiddenKey(key) || 1670 | !this.isValidKey(key) 1671 | } 1672 | 1673 | _entryTypeOf (key) { 1674 | return containerTypeCodes.service 1675 | } 1676 | 1677 | async itemType (itemPath) { 1678 | debug('%s.itemType(\'%s\')', this.constructor.name, itemPath) 1679 | let type = containerTypeCodes.notValid 1680 | try { 1681 | let itemKey = this._subTree + itemPath 1682 | let value = await this.getEntryValue(itemKey) 1683 | if (value) { // itemPath exact match with entry key, so determine entry type for this container 1684 | type = this._entryTypeOf(itemKey) 1685 | } else if (this.isSelf(itemPath)) { 1686 | type = containerTypeCodes.servicesContainer 1687 | } else { 1688 | type = containerTypeCodes.childContainerItem 1689 | // TODO delete old code: 1690 | // WAS:// Attempt to call itemType on a child container 1691 | // type = await this._callFunctionOnItem(itemPath, 'itemType') 1692 | } 1693 | // TODO test the above four lines of code with services that 1694 | // don't have an NFS container as their value, to see if 1695 | // the following stricter code is needed... 1696 | // 1697 | // This is a stricter alternative to the above final 'else' 1698 | // but it may be ok to handle the error. 1699 | // } else { 1700 | // // Check for NFS container 1701 | // let serviceKey = itemPath.split('/')[0] 1702 | // let serviceProperties = this._safeJs.decodeServiceKey(serviceKey) 1703 | // if (serviceProperties && this._isContainerService(serviceProperties.serviceId)) { 1704 | // // Service with container, so pass to child 1705 | // debug('%s has a container: %s', itemPath, type) 1706 | // return await this._callFunctionOnItem(itemPath, 'itemType') 1707 | // } 1708 | } catch (e) { 1709 | type = containerTypeCodes.notValid 1710 | debug('file not found') 1711 | error(e) 1712 | } 1713 | 1714 | return type 1715 | } 1716 | 1717 | // TODO use safeJs service support when resuming that code 1718 | _isContainerService (serviceId) { 1719 | return (serviceId === 'www' || serviceId === 'ldp') 1720 | } 1721 | 1722 | async itemInfo (itemPath) { 1723 | debug('%s.itemInfo(\'%s\')', this.constructor.name, itemPath) 1724 | try { 1725 | if (this.isSelf(itemPath)) { 1726 | return this._safeJs.mutableDataStats(this._mData) 1727 | } else { 1728 | let serviceProperties = this._safeJs.decodeServiceKey(itemPath) 1729 | if (serviceProperties && this._isContainerService(serviceProperties.name)) { 1730 | // Pass to the child container 1731 | return this._callFunctionOnItem(itemPath, 'itemInfo') 1732 | } else { 1733 | debug('Unrecognised service: ', itemPath) 1734 | } 1735 | } 1736 | } catch (e) { error(e) } 1737 | } 1738 | 1739 | /** 1740 | * Get attributes of a file or directory 1741 | * @param {String} itemPath 1742 | * @param {Number} fd [optional] file descriptor (if file is open) 1743 | * 1744 | * @return {Promise} Object { status: C.SUCCESS or an Error object 1745 | * attributes: an attibutes object } 1746 | */ 1747 | async itemAttributes (itemPath, fd) { 1748 | debug('%s.itemAttributes(\'%s\', %s)', this.constructor.name, itemPath, fd) 1749 | 1750 | try { 1751 | let result = await this.itemAttributesResultsRef(itemPath, fd) 1752 | if (result.status === C.SUCCESS) { 1753 | return { status: C.SUCCESS, attributes: result.resultsRef.result } 1754 | } 1755 | return result 1756 | } catch (e) { 1757 | error(e) 1758 | return { status: e } 1759 | } 1760 | } 1761 | 1762 | /* 1763 | * @return {Promise} Object { status: C.SUCCESS or an Error object 1764 | * resultsRef: Object } 1765 | */ 1766 | async itemAttributesResultsRef (itemPath) { 1767 | debug('%s.itemAttributesResultsRef(\'%s\')', this.constructor.name, itemPath) 1768 | let fileOperation = 'itemAttributes' 1769 | 1770 | let type 1771 | let result 1772 | let resultsRef // Will be set if result is from child container (and so uses child's cache) 1773 | const now = Date.now() 1774 | try { 1775 | if (this.isSelf(itemPath)) { 1776 | type = containerTypeCodes.servicesContainer 1777 | debug('%s is type: %s', itemPath, type) 1778 | await this.updateMetadata() 1779 | result = { 1780 | // TODO improve this if SAFE accounts ever have suitable values for size etc: 1781 | modified: now, 1782 | accessed: now, 1783 | created: now, 1784 | size: this._metadata.size, 1785 | version: this._metadata.version, 1786 | 'isFile': false, 1787 | entryType: type 1788 | } 1789 | } 1790 | 1791 | if (!result) { 1792 | type = containerTypeCodes.service 1793 | let serviceProperties = this._safeJs.decodeServiceKey(itemPath) 1794 | if (serviceProperties && this._isContainerService(serviceProperties.serviceId)) { 1795 | // Service with container, so pass to child 1796 | debug('%s has a container: %s', itemPath, type) 1797 | resultsRef = await this._callFunctionOnItem(itemPath, 'itemAttributesResultsRef') 1798 | } 1799 | } 1800 | 1801 | if (!result) { 1802 | // Default values (used as is for containerTypeCodes.nfsContainer) 1803 | type = containerTypeCodes.service 1804 | // Service without its own container (or unkown service) 1805 | debug('%s is type: %s', itemPath, type) 1806 | result = { 1807 | modified: now, 1808 | accessed: now, 1809 | created: now, 1810 | size: 0, 1811 | version: -1, 1812 | 'isFile': true, 1813 | entryType: type 1814 | } 1815 | } 1816 | } catch (e) { 1817 | error(e) 1818 | return { status: e } 1819 | } 1820 | 1821 | if (!resultsRef) { 1822 | resultsRef = this._cacheResultForPath(itemPath, fileOperation, result) 1823 | } 1824 | 1825 | return { status: C.SUCCESS, 'resultsRef': resultsRef } 1826 | } 1827 | } 1828 | 1829 | /** 1830 | * Wrapper for 'NFS' emulation MutableData 1831 | * 1832 | * TODO NfsContainer - implement private MD (currently only creates public MDs) 1833 | */ 1834 | 1835 | // NOTES: 1836 | // In contrast to the default containers which hold other containers, an 1837 | // NFS container can only hold files, or rather pointers to immutable data. 1838 | // An NFS container cannot hold other containers because it would then 1839 | // be possible to create multiple entries corresponding to a single 1840 | // path (for example, by having folders in _public that 'overlap' 1841 | // with a container within an NFS container. 1842 | // 1843 | // The method for creating public SAFE NFS containers is not currently 1844 | // documented by MaidSafe and is instead defined by the Web Hosting Manager 1845 | // example, here: 1846 | // https://github.com/maidsafe/safe_examples/blob/master/web_hosting_manager/app/safenet_comm/api.js#L150 1847 | // 1848 | // In summary, when the Web Hosting Manager creates an NFS container for a www 1849 | // service it uses newRandomPublic(CONSTANTS.TYPE_TAG.WWW) to create the MutableData 1850 | // container, uses quickSetup to apply metadata as: 1851 | // const metaName = `Service Root Directory for: serviceName.publicName`; 1852 | // const metaDesc = `Has the files hosted for the service: serviceName.publicName`; 1853 | // await servFolder.quickSetup({}, metaName, metaDesc); 1854 | // It then inserts an entry into the _public container with 1855 | // key: service path, such as 'somefolder/lastfolder' (default for www service is '/www-root') 1856 | // value: the XoR-name/address on the network (as a Buffer from from getNameAndTag()) 1857 | 1858 | // TODO extends SafeContainer or something else? 1859 | class NfsContainer extends SafeContainer { 1860 | /** 1861 | * [constructor description] 1862 | * @param {SafenetworkJs} safeJs SafenetworkJS API object 1863 | * @param {String} nameOrKey MD name, or if a parent container is given, the key of the MD entry containing the name 1864 | * @param {String} containerPath where this container appears in the SAFE container tree (e.g. '/_public', '/mds') 1865 | * @param {Object} parent (optional) typically a SafeContainer (ServiceContainer?) but if parent is not defined, nameOrKey must be an XOR address 1866 | * @param {Boolean} isPublic (defaults to true) used only when creating an MD 1867 | */ 1868 | constructor (safeJs, nameOrKey, containerPath, parent, isPublic) { 1869 | super(safeJs, nameOrKey, containerPath, '', parent, nameOrKey, safeJs.SN_TAGTYPE_NFS) 1870 | if (parent) { 1871 | this._parentEntryKey = nameOrKey 1872 | } else { 1873 | this._mdName = nameOrKey 1874 | this._isPublic = isPublic 1875 | } 1876 | } 1877 | 1878 | isDefaultContainer () { return false } 1879 | isPublic () { return this._isPublic } 1880 | 1881 | /** 1882 | * Initialise by accessing existing MutableData compatible with NFS emulation 1883 | * @return {Promise} 1884 | */ 1885 | async initialiseExisting () { 1886 | if (!this._parent) { 1887 | throw new Error('TODO add support for XOR nameOrKey to NfsContainer') 1888 | } 1889 | 1890 | try { 1891 | if (this._parent) { 1892 | let valueVersion = await this._parent.getEntry(this._parentEntryKey) 1893 | this._mdName = valueVersion.value 1894 | } 1895 | 1896 | this._mData = await this._safeJs.mutableData.newPublic(this._mdName, this._tagType) 1897 | } catch (err) { 1898 | let info = (this._parent ? this._parent.name + '/' + this._parentEntryKey : this._mdName) 1899 | debug('NfsContainer failed to init existing MD for ' + info) 1900 | debug(err.message) 1901 | } 1902 | } 1903 | 1904 | async initialiseNfs () { 1905 | if (!this._nfs) { 1906 | this._nfs = await this._mData.emulateAs('NFS') 1907 | this._files = new _NfsContainerFiles(this, this._safeJs, this._mData, this._nfs) 1908 | } 1909 | return this._nfs 1910 | } 1911 | 1912 | nfs () { return this._nfs } 1913 | files () { return this._files } 1914 | 1915 | /** 1916 | * create an NFS MutableData and insert into a parent container if present 1917 | * @param {String} ownerName (optional) if provided, usually the public name on which this folder is used 1918 | * @param {Boolean} isPublic (optional) if present overrides constructor 1919 | * @return {Promise} a newly created {MutableData} 1920 | */ 1921 | async createNew (ownerName, isPublic) { 1922 | if (!ownerName) ownerName = '' 1923 | if (!isPublic) isPublic = this._isPublic 1924 | let containerName = (this._parent ? this._parent._name : '') 1925 | return this._safeJs.createNfsContainerMd(containerName, ownerName, this._mdName, this._tagType, !isPublic) 1926 | } 1927 | 1928 | /** 1929 | * create a general purpose NFS container in _public 1930 | * @param {Number} tagType (optional) SAFE MutableData tagType 1931 | * @param {String} metaName (optional) metadata describing what this is for 1932 | * @param {String} metaDescription (optional) metadata explaining what this contains 1933 | * @return {Promise} ??? 1934 | */ 1935 | // TODO create a general purpose NFS container in _public 1936 | async createPublicFolder (tagType, metaName, metaDescription) { 1937 | throw new Error('createPublicFolder() not yet implemented') 1938 | // if (!tagType) tagType = ??? 1939 | // if (!metaName) metaName = ??? 1940 | // if (!metaDescription) metaDescription = ??? 1941 | } 1942 | 1943 | async itemInfo (itemPath) { 1944 | debug('%s.itemInfo(\'%s\')', this.constructor.name, itemPath) 1945 | try { 1946 | if (this.isSelf(itemPath)) { 1947 | return this._safeJs.mutableDataStats(this._mData) 1948 | } else if (this.itemType(itemPath) === containerTypeCodes.fakeContainer) { 1949 | return { 1950 | // TODO consider using listFolder to count folders, recursing, and then adding info from files 1951 | // TODO these members are junk (inherited from IPFS code so change them!) 1952 | repoSize: 12345, 1953 | storageMax: 99999, 1954 | numObjects: 321 1955 | } 1956 | } else { 1957 | // Pass to the child container 1958 | return this._callFunctionOnItem(itemPath, 'itemInfo') 1959 | } 1960 | } catch (e) { error(e) } 1961 | } 1962 | 1963 | _entryTypeOf (key) { 1964 | return containerTypeCodes.file 1965 | } 1966 | 1967 | async itemType (itemPath) { 1968 | debug('%s.itemType(\'%s\')', this.constructor.name, itemPath) 1969 | let type 1970 | 1971 | try { 1972 | let fileState = await this._files._fetchFileState(itemPath, /* fromNetwork */ true) 1973 | if (fileState) { 1974 | if (!fileState.isDeletedFile()) { 1975 | type = containerTypeCodes.file 1976 | } 1977 | this._files._destroyFileState(fileState) 1978 | } 1979 | // Check for a defaultContainer or fakeContainer 1980 | if (!type) { 1981 | let itemKey = this._subTree + itemPath 1982 | let value = await this.getEntryValue(itemKey) 1983 | if (value) { // itemPath exact match with entry key, so determine entry type for this container 1984 | type = this._entryTypeOf(itemKey) 1985 | } else if (this.isSelf(itemPath)) { 1986 | type = containerTypeCodes.defaultContainer 1987 | } else { 1988 | // Check for fakeContainer or NFS container 1989 | let itemAsFolder = (u.isNfsFolder(itemPath) ? itemPath : itemPath + '/') 1990 | let shortestEnclosingKey = await this._getShortestEnclosingKey(itemAsFolder) 1991 | if (shortestEnclosingKey) { 1992 | type = containerTypeCodes.fakeContainer 1993 | } else if (!type) { 1994 | type = containerTypeCodes.notFound 1995 | } 1996 | } 1997 | } 1998 | } catch (e) { 1999 | type = containerTypeCodes.notValid 2000 | debug('file not found') 2001 | error(e) 2002 | } 2003 | 2004 | debug('%s is type: ', itemPath, type) 2005 | return type 2006 | } 2007 | 2008 | /** 2009 | * Get attributes of a file or directory 2010 | * @param {String} itemPath 2011 | * @param {Number} fd [optional] file descriptor (if file is open) 2012 | * 2013 | * @return {Promise} Object { status: C.SUCCESS or an Error object 2014 | * attributes: an attibutes object } 2015 | */ 2016 | async itemAttributes (itemPath, fd) { 2017 | debug('%s.itemAttributes(\'%s\', %s)', this.constructor.name, itemPath, fd) 2018 | 2019 | try { 2020 | let result = await this.itemAttributesResultsRef(itemPath, fd) 2021 | if (result.status === C.SUCCESS) { 2022 | return { status: C.SUCCESS, attributes: result.resultsRef.result } 2023 | } 2024 | return result 2025 | } catch (e) { 2026 | error(e) 2027 | return { status: e } 2028 | } 2029 | } 2030 | 2031 | /** 2032 | * Get attributes of a file or directory as a resultsRef object 2033 | * @param {String} itemPath 2034 | * @param {Number} fd [optional] file descriptor (if file is open) 2035 | * 2036 | * @return {Promise} Object { status: C.SUCCESS or an Error object 2037 | * resultsRef: Object } 2038 | */ 2039 | async itemAttributesResultsRef (itemPath, fd) { 2040 | debug('%s.itemAttributesResultsRef(\'%s\', %s)', this.constructor.name, itemPath, fd) 2041 | let fileOperation = 'itemAttributes' 2042 | let resultsRef // Will be set if result is from child (and so cached by child) 2043 | 2044 | // Look for a cached resultsRef 2045 | let resultHolder = this._resultHolderMap[itemPath] 2046 | if (resultHolder && resultHolder[fileOperation]) { 2047 | resultsRef = { 2048 | resultsMap: this._resultHolderMap, 2049 | resultsKey: itemPath, 2050 | result: resultHolder[fileOperation], 2051 | 'fileOperation': fileOperation // For debugging only 2052 | } 2053 | return { status: C.SUCCESS, 'resultsRef': resultsRef } 2054 | } 2055 | 2056 | let result 2057 | const now = Date.now() 2058 | try { 2059 | if (this.isSelf(itemPath)) { 2060 | await this.updateMetadata() 2061 | result = { 2062 | // TODO improve this if SAFE accounts ever have suitable values for size etc: 2063 | modified: now, 2064 | accessed: now, 2065 | created: now, 2066 | size: this._metadata.size, 2067 | version: this._metadata.version, 2068 | 'isFile': false, 2069 | entryType: containerTypeCodes.nfsContainer 2070 | } 2071 | debug('%s is type: %s', itemPath, result.entryType) 2072 | } 2073 | 2074 | if (!result) { 2075 | let type 2076 | let fileState 2077 | if (fd) { 2078 | fileState = await this._files.getFileStateForDescriptor(fd) 2079 | } else { 2080 | fileState = await this._files._fetchFileState(itemPath, /* fromNetwork */ true) 2081 | } 2082 | 2083 | if (fileState && !fileState.isDeletedFile()) { 2084 | type = containerTypeCodes.file 2085 | } 2086 | type = await this.itemType(itemPath) 2087 | 2088 | if (type === containerTypeCodes.file) { 2089 | // File (or new file if fileState._fileFetched is undefined) 2090 | let file = fileState._fileFetched 2091 | result = { 2092 | modified: file ? Number(file.modified) : now, 2093 | accessed: now, 2094 | created: file ? Number(file.created) : now, 2095 | size: file ? await fileState._fileFetched.size() : 0, 2096 | version: file ? file.version : 0, 2097 | 'isFile': true, 2098 | entryType: type 2099 | } 2100 | } else if (type === containerTypeCodes.fakeContainer) { 2101 | // Fake container 2102 | // Default values (used as is for containerTypeCodes.nfsContainer) 2103 | result = { 2104 | modified: now, 2105 | accessed: now, 2106 | created: now, 2107 | size: 0, 2108 | version: -1, 2109 | 'isFile': false, 2110 | entryType: type 2111 | } 2112 | } else if (type === containerTypeCodes.notFound) { 2113 | result = { entryType: containerTypeCodes.notFound } 2114 | } else { 2115 | throw new Error('Unexpected itemType: ' + type) 2116 | } 2117 | } 2118 | } catch (e) { 2119 | error(e) 2120 | return { status: e } 2121 | } 2122 | 2123 | debug('%s is type: %s', itemPath, result.entryType) 2124 | if (!resultsRef) { 2125 | resultsRef = this._cacheResultForPath(itemPath, fileOperation, result) 2126 | } 2127 | 2128 | return { status: C.SUCCESS, 'resultsRef': resultsRef } 2129 | } 2130 | 2131 | /** 2132 | * Open a file for read or write 2133 | * 2134 | * @param {String} itemPath 2135 | * @param {Number} nfsFlags SAFE NFS API open() flags 2136 | * 2137 | * @return {Promise} Object { status: C.SUCCESS or an Error object 2138 | * fileDescriptor: an integar >0 on success } 2139 | */ 2140 | async openFile (itemPath, nfsFlags) { 2141 | debug('%s.openFile(\'%s\', %s)', this.constructor.name, itemPath, nfsFlags) 2142 | try { 2143 | return await this._files.openFile(itemPath, nfsFlags) 2144 | } catch (e) { 2145 | error(e) 2146 | return e 2147 | } 2148 | } 2149 | 2150 | /** 2151 | * Create a file. 2152 | * 2153 | * @param {String} itemPath 2154 | * 2155 | * @return {Promise} Object { status: C.SUCCESS or an Error object 2156 | * fileDescriptor: an integar >0 on success } 2157 | */ 2158 | async createFile (itemPath) { 2159 | debug('%s.createFile(\'%s\')', this.constructor.name, itemPath) 2160 | let result 2161 | try { 2162 | result = await this._files.createFile(itemPath) 2163 | // This is ugly. 2164 | // After createFile(), but before closeFile() we fake the file's existence 2165 | // so that itemAttributes() can be used to check the createFile() 2166 | // succeeded. This is done by inserting an itemAttributes() result 2167 | // into the cache of this container *and* its parent container 2168 | if (result.status === C.SUCCESS) { 2169 | let attributes = this._newFileAttributes() 2170 | this._cacheResultForPath(itemPath, 'itemAttributes', attributes) 2171 | } 2172 | return result 2173 | } catch (e) { 2174 | error(e) 2175 | return e 2176 | } 2177 | } 2178 | 2179 | // Helper to cache an itemAttributes() entry for a new file, pending closeFile() 2180 | _newFileAttributes () { 2181 | let now = Date() 2182 | return { 2183 | modified: now, 2184 | accessed: now, 2185 | created: now, 2186 | size: 0, 2187 | version: 0, 2188 | 'isFile': true, 2189 | entryType: containerTypeCodes.newFile 2190 | } 2191 | } 2192 | 2193 | /** 2194 | * Close file and save to network 2195 | * @param {String} itemPath 2196 | * @param {Number} fd 2197 | * 2198 | * @return {Promise} Object { status: C.SUCCESS or an Error object } 2199 | */ 2200 | async closeFile (itemPath, fd) { 2201 | debug('%s.closeFile(\'%s\', %s)', this.constructor.name, itemPath, fd) 2202 | try { 2203 | return await this._files.closeFile(itemPath, fd) 2204 | } catch (e) { 2205 | error(e) 2206 | return e 2207 | } 2208 | } 2209 | 2210 | /** 2211 | * Delete file 2212 | * @param {String} itemPath 2213 | * 2214 | * @return {Promise} Object { status: C.SUCCESS or an Error object, 2215 | * wasLastItem: true if itemPath folder left emtpy } 2216 | */ 2217 | async deleteFile (itemPath) { 2218 | debug('%s.deleteFile(\'%s\')', this.constructor.name, itemPath) 2219 | try { 2220 | return this._files.deleteFile(itemPath) 2221 | } catch (e) { 2222 | error(e) 2223 | return { status: e } 2224 | } 2225 | } 2226 | 2227 | /** 2228 | * rename a file and/or move between paths within this container 2229 | * 2230 | * @param {String} itemPath 2231 | * @param {String} newItemPath 2232 | * @return {Promise} Object { result: true on success, 2233 | * wasLastItem: true if itemPath folder left emtpy } 2234 | */ 2235 | 2236 | /** 2237 | * Rename/move a file (within the same container) 2238 | * 2239 | * Note: SafenetworkApi.moveFile() supports rename/move between containers 2240 | * 2241 | * @param {String} sourcePath 2242 | * @param {String} destinationPath 2243 | * 2244 | * @return {Promise} Object { status: C.SUCCESS or an Error object 2245 | * wasLastItem: true if itemPath folder left emtpy } 2246 | */ 2247 | 2248 | // Note: FUSE or the file system appears to check validity of the 2249 | // operation before calling rename() so we can just concentrate 2250 | // on implementing operations that we support. 2251 | // 2252 | // For now, we will only support rename() within a single container, 2253 | // and only of a filename rather than a directory. The reason 2254 | // for this is that I am advocating possible changes in implementation 2255 | // here: https://forum.safedev.org/t/proposal-to-change-implementation-of-safe-nfs/2111?u=happybeing 2256 | // 2257 | // POSIX Ref: http://pubs.opengroup.org/onlinepubs/9699919799/ 2258 | async moveFile (itemPath, newItemPath) { 2259 | debug('%s.moveFile(\'%s\', \'%s\')', this.constructor.name, itemPath, newItemPath) 2260 | 2261 | let result 2262 | try { 2263 | // Don't allow renaming directories because it can use up a lot of entries fast 2264 | // See: https://forum.safedev.org/t/proposal-to-change-implementation-of-safe-nfs/2111?u=happybeing 2265 | let srcIsFile = await this.isActiveKey(itemPath) 2266 | if (!srcIsFile) throw new Error('cannot rename a directory') 2267 | 2268 | // Make newItemPath relative to container root (itemPath is already relative) 2269 | let trimmedNewPath = newItemPath 2270 | if (this._parentEntryKey) trimmedNewPath = (this._parent._subTree + newItemPath).substring(this._parentEntryKey.length + 1) 2271 | 2272 | if (itemPath === trimmedNewPath) { // Rename to self does nothing 2273 | return { status: C.SUCCESS, wasLastItem: false } 2274 | } 2275 | 2276 | return await this._files.moveFile(itemPath, trimmedNewPath) 2277 | } catch (e) { 2278 | error(e) 2279 | return { status: e } 2280 | } 2281 | } 2282 | 2283 | /** 2284 | * Get user metadata for a file (file does not need to be open) 2285 | * 2286 | * @param {Number} fd [optional] file descriptor obtained from openFile() or createFile() 2287 | * 2288 | * 2289 | * @return {Promise} Object { status: C.SUCCESS or an Error object 2290 | * metadata: a buffer containing any metadata as previously set } 2291 | */ 2292 | async getFileMetadata (itemPath, fd) { 2293 | try { 2294 | return this._files.getFileMetadata(itemPath, fd) 2295 | } catch (e) { 2296 | error(e) 2297 | return { status: e } 2298 | } 2299 | } 2300 | /** 2301 | * Set metadata to be written when on closeFile() (for a file opened for write) 2302 | * 2303 | * Note: must only be called after succesful createFile() or openFile() for write 2304 | * @param {String} itemPath 2305 | * @param {Number} fd [optional] file descriptor 2306 | * @param {Buffer} metadata Metadata that will be written on closeFile() 2307 | * 2308 | * @return {Promise} C.SUCCESS or an Error object 2309 | */ 2310 | async setFileMetadata (itemPath, fd, metadata) { 2311 | try { 2312 | return this._files.setFileMetadata(itemPath, fd, metadata) 2313 | } catch (e) { 2314 | error(e) 2315 | return e 2316 | } 2317 | } 2318 | 2319 | /** 2320 | * Read up to len bytes starting from pos 2321 | * 2322 | * This function can be used in one of two ways: 2323 | * - simple: just call readFile() and it will read data, and if the 2324 | * file is not open yet, it will do that first 2325 | * - you can call openFile() before, to open in a specific mode using flags 2326 | * 2327 | * Note: if this function fails, the cached file state is purged and any file 2328 | * descriptor will be invalidated 2329 | * 2330 | * @param {String} itemPath path (key) of the file (in container which owns this _NfsContainerFiles) 2331 | * @param {Number} fd [optional] file descriptor obtained from openFile() 2332 | * @param {Number} pos (Number | C.NFS_FILE_START) 2333 | * @param {Number} len (Number | C.NFS_FILE_END) 2334 | * 2335 | * @return {Promise} Object { status: C.SUCCESS or an Error object, 2336 | * content: String containing the bytes read } 2337 | */ 2338 | async readFile (itemPath, fd, pos, len) { 2339 | debug('%s.readFile(\'%s\', %s, %s, %s)', this.constructor.name, itemPath, fd, pos, len) 2340 | try { 2341 | return this._files.readFile(itemPath, fd, pos, len) 2342 | } catch (e) { 2343 | error(e) 2344 | return { status: e } 2345 | } 2346 | } 2347 | 2348 | /** 2349 | * Read up to len bytes into buf (Uint8Array), starting at pos 2350 | * 2351 | * This function can be used in one of two ways: 2352 | * - simple: just call readFileBuf() and it will read data, and if the 2353 | * file is not open yet, it will do that first 2354 | * - you can call openFile() before, to open in a specific mode using flags 2355 | * 2356 | * Note: if this function fails, the cached file state is purged and any file 2357 | * descriptor will be invalidated 2358 | * 2359 | * @param {String} itemPath path (key) of the file (in container which owns this _NfsContainerFiles) 2360 | * @param {Number} fd [optional] file descriptor obtained from openFile() 2361 | * @param {Uint8Array} buf [description] 2362 | * @param {Number} pos (Number | CONSTANTS.NFS_FILE_START) 2363 | * @param {Number} len (Number | CONSTANTS.NFS_FILE_END) 2364 | * @return {Promise} Object { status: C.SUCCESS or an Error object, 2365 | * bytes: Integer number of bytes read } 2366 | */ 2367 | async readFileBuf (itemPath, fd, buf, pos, len) { 2368 | debug('%s.readFileBuf(\'%s\', buf, %s, %s)', this.constructor.name, itemPath, pos, len) 2369 | try { 2370 | return this._files.readFileBuf(itemPath, fd, buf, pos, len) 2371 | } catch (e) { 2372 | error(e) 2373 | return { status: e } 2374 | } 2375 | } 2376 | 2377 | /** 2378 | * Write up to len bytes starting from pos 2379 | * 2380 | * This function can be used in one of two ways: 2381 | * - simple: just call writeFile() and it will write data, and if the 2382 | * file is not open yet, it will do that first 2383 | * - you can call openFile() before, to open in a specific mode using flags 2384 | * 2385 | * Note: if this function fails, the cached file state is purged and any file 2386 | * descriptor will be invalidated 2387 | * 2388 | * @param {String} itemPath path (key) of the file (in container which owns this _NfsContainerFiles) 2389 | * @param {Number} fd [optional] file descriptor obtained from openFile() 2390 | * @param {Buffer|String} content (Number | CONSTANTS.NFS_FILE_END) 2391 | * 2392 | * @return {Promise} Object { status: C.SUCCESS or an Error object, 2393 | * bytes: Integer number of bytes written } 2394 | */ 2395 | async writeFile (itemPath, fd, content) { 2396 | debug('%s.writeFile(\'%s\', %s, ...)', this.constructor.name, itemPath, fd) 2397 | try { 2398 | return this._files.writeFile(itemPath, fd, content) 2399 | } catch (e) { 2400 | error(e) 2401 | return { status: e } 2402 | } 2403 | } 2404 | 2405 | /** 2406 | * Write to file, len bytes from buf (Uint8Array) 2407 | * 2408 | * This function can be used in one of two ways: 2409 | * - simple: just call writeFileBuf() and it will write data, and if the 2410 | * file is not open yet, it will do that first 2411 | * - you can call openFile() before, to open in a specific mode using flags 2412 | * 2413 | * Note: if this function fails, the cached file state is purged and any file 2414 | * descriptor will be invalidated 2415 | * 2416 | * @param {String} itemPath path (key) of the file (in container which owns this _NfsContainerFiles) 2417 | * @param {Number} fd [optional] file descriptor obtained from openFile() 2418 | * @param {Uint8Array} buf [description] 2419 | * @param {Number} len 2420 | * @param {Number} pos [optional] position of file to write (must not be less than end of last write) 2421 | * 2422 | * @return {Promise} Object { status: C.SUCCESS or an Error object, 2423 | * bytes: Integer number of bytes written } 2424 | */ 2425 | async writeFileBuf (itemPath, fd, buf, len, pos) { 2426 | debug('%s.writeFileBuf(\'%s\', %s, buf, %s, %s)', this.constructor.name, itemPath, fd, len, pos) 2427 | 2428 | try { 2429 | return this._files.writeFileBuf(itemPath, fd, buf, len, pos) 2430 | } catch (e) { 2431 | error(e) 2432 | return { status: e } 2433 | } 2434 | } 2435 | 2436 | /** 2437 | * Truncate a file to size bytes (only implements size === 0) 2438 | * 2439 | * @private This function is implemented purely to allow FUSE to open a 2440 | * file for append, but overwrite it by first truncating its size to zero. 2441 | * This is needed because POSIX open() only has flags for write, not for 2442 | * append. But since SAFE NFS lacks file truncate, we can only truncate 2443 | * to zero which we do be creating a new file with NFS open(). 2444 | * 2445 | * When opening a SAFE NFS file for write we must 'append', otherwise FUSE 2446 | * would have now way to append (since it can only open() for write, not 2447 | * write with append). In turn, the ony way to allow FUSE to be able to open 2448 | * and overwrite an existing NFS file is to implement truncate at size zero. 2449 | * 2450 | * @param {String} itemPath 2451 | * @param {Number} fd [optional] if omitted, truncates based on itemPath 2452 | * @param {Number} size 2453 | * 2454 | * @return {Promise} C.SUCCESS or an Error object 2455 | */ 2456 | async _truncateFile (itemPath, fd, size) { 2457 | debug('%s._truncateFile(\'%s\', %s, %s)', this.constructor.name, itemPath, fd, size) 2458 | try { 2459 | if (size !== 0) throw new Error('_truncateFile() not implemented for size other than zero') 2460 | return this._files._truncateFile(itemPath, fd, size) 2461 | } catch (e) { 2462 | error(e) 2463 | return e 2464 | } 2465 | } 2466 | 2467 | /** 2468 | * create an NFS container in _public for a SAFE service 2469 | * @param {number} tagType (optional) SAFE MutableData tagType (defaults to www) 2470 | * @param {String} servicePath (optional) Defaults to @www 2471 | * @param {String} metaFor will be of `serviceName.publicName` format 2472 | * @return {Promise} ??? 2473 | */ 2474 | // When to use the container classes, SafenetworkApi or ServicesInterface classes? 2475 | // 2476 | // The SafeContainer, ServicesContainer, NfsContainer etc classes provide 2477 | // a simple API to manage the main SAFE API types and their containers, while 2478 | // the classes and methods which they rely on (including methods on SafenetworkApi 2479 | // and the ServiceInteface class) provide greater control and additional 2480 | // functionality such as SAFE services accessible via fetch() in web 2481 | // applications, and possibly also on desktop. For example, a web library which 2482 | // uses fetch() to access RESTful web services could be used without 2483 | // modification along with SafenetworkJs to access those services on 2484 | // SAFE Network, if implemented using a custom ServiceInterface (cf. SafeServiceLDP) 2485 | // 2486 | // So if you just want to access and create standard SAFE containers and 2487 | // data types such as public names or share public files and websites, 2488 | // the container classes are intended to do everything you might need. 2489 | // You can still dip into the other methods where needed. 2490 | 2491 | // TODO create an NFS container in _public for a SAFE service 2492 | // TODO modify this to use the service classes and service configs rather than have duplicate code 2493 | // TODO I think servicePath might change - check status with Gabriel re my proposals 2494 | // TODO here: https://forum.safedeentry.value.org/t/proposals-for-restful-service-handling/1550 2495 | // TODO Gabriel had some responses/alternatives which I liked but can't find those. 2496 | async createServiceFolder (tagType, servicePath, metaFor) { 2497 | throw new Error('createServiceFolder() not yet implemented') 2498 | // if (!tagType) tagType = safeApi.CONSTANTS.TYPE_TAG.WWW 2499 | // if (!metaName) metaName = `Service Root Directory for: ${metaFor}` 2500 | // if (!metaDescription) metaDescription = `Has the files hosted for the service: ${metaFor}` 2501 | 2502 | // TODO implement - see TO DO notes above 2503 | } 2504 | } 2505 | 2506 | // TODO use these to create list of default permissions to request on auth (bootstrap) 2507 | // Map of SAFE default container names to wrapper class 2508 | const defaultContainers = { 2509 | '_public': PublicContainer, 2510 | '_documents': PrivateContainer, 2511 | '_photos': PrivateContainer, 2512 | '_music': PrivateContainer, 2513 | '_video': PrivateContainer, 2514 | '_publicNames': PublicNamesContainer 2515 | } 2516 | 2517 | // Container classes are all default containers plus... 2518 | const containerClasses = defaultContainers 2519 | containerClasses._services = ServicesContainer 2520 | 2521 | module.exports.defaultContainerNames = defaultContainerNames 2522 | module.exports.containerTypeCodes = containerTypeCodes 2523 | module.exports.isCacheableResult = isCacheableResult 2524 | module.exports.defaultContainers = defaultContainers 2525 | module.exports.containerClasses = containerClasses 2526 | module.exports.SafeContainer = SafeContainer 2527 | module.exports.NfsContainer = NfsContainer 2528 | -------------------------------------------------------------------------------- /src/safenetwork-fs.js: -------------------------------------------------------------------------------- 1 | /* 2 | * SAFEnetwork FS API - filesystem support on top of SAFE Application API (based on SAFE NFS) 3 | * 4 | 5 | TODO theWebalyst: 6 | see safenetwork-api.js for TODOs! 7 | */ 8 | -------------------------------------------------------------------------------- /src/safenetwork-utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Local helpers 3 | */ 4 | 5 | const path = require('path') // Cross platform itemPath handling 6 | 7 | const isFolder = function (itemPath, separator) { 8 | if (!separator) separator = path.sep 9 | return (itemPath.slice(-1) === separator) 10 | } 11 | 12 | const isNfsFolder = function (itemPath) { 13 | return itemPath.slice(-1) === '/' 14 | } 15 | 16 | // Strip fragment for URI (removes everything from first '#') 17 | const docpart = function (uri) { 18 | var i 19 | i = uri.indexOf('#') 20 | if (i < 0) { 21 | return uri 22 | } else { 23 | return uri.slice(0, i) 24 | } 25 | } 26 | 27 | // Return full document itemPath from root (strips host and fragment) 28 | const itemPathPart = function (uri) { 29 | let hostlen = hostpart(uri).length 30 | uri = uri.slice(protocol(uri).length) 31 | if (uri.indexOf('://') === 0) { 32 | uri = uri.slice(3) 33 | } 34 | return docpart(uri.slice(hostlen)) 35 | } 36 | 37 | const hostpart = function (uri) { 38 | var m = /[^\/]*\/\/([^\/]*).*/.exec(uri) 39 | if (m) { 40 | return m[1] 41 | } else { 42 | return '' 43 | } 44 | } 45 | 46 | const protocol = function (uri) { 47 | var i 48 | i = uri.indexOf(':') 49 | if (i < 0) { 50 | return null 51 | } else { 52 | return uri.slice(0, i) 53 | } 54 | } 55 | 56 | const parentPath = function (itemPath) { 57 | return path.dirname(itemPath) 58 | } 59 | 60 | // Return '' rather than '.' for current directory 61 | const parentPathNoDot = function (itemPath) { 62 | let parentPath = path.dirname(itemPath) 63 | if (parentPath === '.') parentPath = '' 64 | return parentPath 65 | } 66 | 67 | // Used to cache file info 68 | const Cache = function (maxAge) { 69 | this.maxAge = maxAge 70 | this._items = {} 71 | } 72 | 73 | // Cache of file version info 74 | Cache.prototype = { 75 | get: function (key) { 76 | var item = this._items[key] 77 | var now = new Date().getTime() 78 | // Google backend expires cached fileInfo, so we do too 79 | // but I'm not sure if this is helpful. No harm tho. 80 | return (item && item.t >= (now - this.maxAge)) ? item.v : undefined 81 | }, 82 | 83 | set: function (key, value) { 84 | this._items[key] = { 85 | v: value, 86 | t: new Date().getTime() 87 | } 88 | }, 89 | 90 | 'delete': function (key) { 91 | if (this._items[key]) { 92 | delete this._items[key] 93 | } 94 | } 95 | } 96 | 97 | /* 98 | * Adapted from node-solid-server/lib/metadata.js 99 | */ 100 | 101 | class LdpMetadata { 102 | constructor() { 103 | this.filename = '' 104 | this.isResource = false 105 | this.isSourceResource = false 106 | this.isContainer = false 107 | this.isBasicContainer = false 108 | this.isDirectContainer = false 109 | } 110 | } 111 | 112 | 113 | function ldpMetadata () { 114 | this.filename = '' 115 | this.isResource = false 116 | this.isSourceResource = false 117 | this.isContainer = false 118 | this.isBasicContainer = false 119 | this.isDirectContainer = false 120 | } 121 | 122 | /* 123 | * Adapted from node-solid-server/lib/headers.js 124 | */ 125 | 126 | function addLink (headers, value, rel) { 127 | var oldLink = headers.get('Link') 128 | if (oldLink === undefined) { 129 | headers.set('Link', '<' + value + '>; rel="' + rel + '"') 130 | } else { 131 | headers.set('Link', oldLink + ', ' + '<' + value + '>; rel="' + rel + '"') 132 | } 133 | } 134 | 135 | function addLinks (headers, fileMetadata) { 136 | if (fileMetadata.isResource) { 137 | addLink(headers, 'http://www.w3.org/ns/ldp#Resource', 'type') 138 | } 139 | if (fileMetadata.isSourceResource) { 140 | addLink(headers, 'http://www.w3.org/ns/ldp#RDFSource', 'type') 141 | } 142 | if (fileMetadata.isContainer) { 143 | addLink(headers, 'http://www.w3.org/ns/ldp#Container', 'type') 144 | } 145 | if (fileMetadata.isBasicContainer) { 146 | addLink(headers, 'http://www.w3.org/ns/ldp#BasicContainer', 'type') 147 | } 148 | if (fileMetadata.isDirectContainer) { 149 | addLink(headers, 'http://www.w3.org/ns/ldp#DirectContainer', 'type') 150 | } 151 | } 152 | 153 | /* 154 | * Copied from node-solid-server/lib/utils.js 155 | */ 156 | /** 157 | * Composes and returns the fully-qualified URI for the request, to be used 158 | * as a base URI for RDF parsing or serialization. For example, if a request 159 | * is to `Host: example.com`, `GET /files/` using the `https:` protocol, 160 | * then: 161 | * 162 | * ``` 163 | * getFullUri(req) // -> 'https://example.com/files/' 164 | * ``` 165 | * 166 | * @param req {IncomingMessage} 167 | * 168 | * @return {string} 169 | */ 170 | function getFullUri (req) { 171 | return getBaseUri(req) + url.resolve(req.baseUrl, req.itemPath) 172 | } 173 | 174 | function itemPathBasename (fullitemPath) { 175 | var basename = '' 176 | if (fullitemPath) { 177 | basename = (fullitemPath.lastIndexOf('/') === fullitemPath.length - 1) 178 | ? '' 179 | : fullitemPath.substring(fullitemPath.lastIndexOf('/') + 1) 180 | } 181 | return basename 182 | } 183 | 184 | function hasSuffix (itemPath, suffixes) { 185 | for (var i in suffixes) { 186 | if (itemPath.indexOf(suffixes[i], itemPath.length - suffixes[i].length) !== -1) { 187 | return true 188 | } 189 | } 190 | return false 191 | } 192 | 193 | function filenameToBaseUri (filename, uri, base) { 194 | var uriPath = S(filename).strip(base).toString() 195 | return uri + '/' + uriPath 196 | } 197 | 198 | function getBaseUri (req) { 199 | return req.protocol + '://' + req.get('host') 200 | } 201 | 202 | /* 203 | * npm modules 204 | */ 205 | const S = module.exports.string = require('string') 206 | const url = module.exports.url = require('url') 207 | 208 | module.exports.path = path 209 | /* 210 | * Local helpers 211 | */ 212 | module.exports.isFolder = isFolder 213 | module.exports.isNfsFolder = isNfsFolder 214 | module.exports.docpart = docpart 215 | module.exports.itemPathPart = itemPathPart 216 | module.exports.hostpart = hostpart 217 | module.exports.protocol = protocol 218 | module.exports.parentPath = parentPath 219 | module.exports.parentPathNoDot = parentPathNoDot 220 | module.exports.Cache = Cache 221 | 222 | // Adapted/copied from node-solid-server 223 | module.exports.LdpMetadata = LdpMetadata 224 | module.exports.addLink = addLink 225 | module.exports.addLinks = addLinks 226 | 227 | module.exports.getFullUri = getFullUri 228 | module.exports.itemPathBasename = itemPathBasename 229 | module.exports.hasSuffix = hasSuffix 230 | module.exports.filenameToBaseUri = filenameToBaseUri 231 | module.exports.getBaseUri = getBaseUri 232 | -------------------------------------------------------------------------------- /src/webid_profile.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 MaidSafe.net limited. 2 | // 3 | // This SAFE Network Software is licensed to you under 4 | // the MIT license or 5 | // the Modified BSD license , 6 | // at your option. 7 | // 8 | // This file may not be copied, modified, or distributed except according to those terms. 9 | // 10 | // Please review the Licences for the specific language governing permissions and limitations 11 | // relating to use of the SAFE Network Software. 12 | 13 | /////////////////////////////////////////////////////////////////////////// 14 | // SafenetworkJS WebID support 15 | // 16 | // Modifications Copyright 2019 theWebalyst 17 | // 18 | // 19 | 20 | const { parse: parseUrl } = require('url'); 21 | const CONSTANTS = require('./constants') 22 | 23 | /** 24 | * RDF based API for SAFE WebID (experimental API) 25 | */ 26 | class WebIdProfile { 27 | constructor(safeApp, uri) { 28 | this.safeApp = safeApp 29 | this.uri = uri 30 | let parsedUrl = parseUrl(uri); 31 | if (!parsedUrl.protocol) parsedUrl = parseUrl('safe://' + webId.uri) 32 | const hostParts = parsedUrl.hostname.split('.') 33 | this.publicName = hostParts.pop() // last one is 'publicName' 34 | this.subName = hostParts.join('.') // all others are 'subNames' 35 | this.graphId = `safe://${this.subName}.${this.publicName}`; 36 | } 37 | 38 | /** 39 | * Read WebID Profile from network 40 | * @return {Promise} Mutable Data RDF emulation 41 | */ 42 | async read () { 43 | console.log('WebIdProfile.read()') 44 | if (this.rdf) return this.rdf 45 | 46 | const container = await getContainerFromPublicId(this.safeApp, this.publicName, this.subName) 47 | if (container.type !== DATA_TYPE_RDF) { 48 | throw Error('WebIdProfile ERROR: service container is not RDF') 49 | } 50 | 51 | this.serviceMd = container.serviceMd 52 | this.rdf = await this.serviceMd.emulateAs('RDF') 53 | await this.rdf.nowOrWhenFetched() 54 | this.vocabs = { 55 | LDP: this.rdf.namespace('http://www.w3.org/ns/ldp#'), 56 | RDF: this.rdf.namespace('http://www.w3.org/2000/01/rdf-schema#'), 57 | RDFS: this.rdf.namespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#'), 58 | FOAF: this.rdf.namespace('http://xmlns.com/foaf/0.1/'), 59 | OWL: this.rdf.namespace('http://www.w3.org/2002/07/owl#'), 60 | DCTERMS: this.rdf.namespace('http://purl.org/dc/terms/'), 61 | SAFETERMS: this.rdf.namespace('http://safenetwork.org/safevocab/'), 62 | PIM: this.rdf.namespace('http://www.w3.org/ns/pim/') 63 | } 64 | return this.rdf 65 | } 66 | 67 | /** 68 | * Save RDF WebID profile to network 69 | * @return {Promise}. of the WebID profile Mutable Data 70 | */ 71 | async write () { 72 | console.log('WebIdProfile.write()') 73 | if (!this.rdf) throw Error('Error: profile must be initialised before write') 74 | return this.rdf.commit() 75 | } 76 | 77 | /* 78 | 79 | Commented out as the code to add this to the account is not implemented here 80 | and is not needed yet, because all we are doing is editing an existing 81 | WebID profile. 82 | 83 | async new () { 84 | const md = await this.safeApp.mutableData.newRandomPublic(CONSTANTS.TYPE_TAG.WEBID) 85 | await md.quickSetup({}) 86 | this.rdf = await emulateAs('RDF') 87 | return this.rdf 88 | } 89 | */ 90 | 91 | // TODO move this to the caller - just here for testing 92 | setStorageLocation(storageUri) { 93 | console.log('WebIdProfile.setStorageLocation(' + storageUri + ')') 94 | const rdf = this.rdf 95 | const hasMeAlready = this.uri.includes('#me'); 96 | const webIdSym = hasMeAlready ? rdf.sym(this.uri) : rdf.sym(`${this.uri}#me`); 97 | 98 | rdf.removeMany(webIdSym, this.vocabs.PIM('space#storage'), null); 99 | rdf.add(webIdSym, this.vocabs.PIM('space#storage'), rdf.sym(storageUri)); 100 | } 101 | 102 | getStorageLocation() { 103 | console.log('WebIdProfile.getStorageLocation()') 104 | const rdf = this.rdf 105 | const hasMeAlready = this.uri.includes('#me'); 106 | const webIdSym = hasMeAlready ? rdf.sym(this.uri) : rdf.sym(`${this.uri}#me`); 107 | 108 | const match = rdf.statementsMatching(webIdSym, this.vocabs.PIM('space#storage'), undefined) 109 | const storageUri = match[0].object.value.split(',') 110 | console.log('returning: ', storageUri) 111 | return storageUri 112 | } 113 | } 114 | 115 | // From maidsafe/safe_app_nodejs/src/web_fetch.js 116 | const errConst = require('./error_const'); 117 | 118 | const DATA_TYPE_NFS = 'NFS'; 119 | const DATA_TYPE_RDF = 'RDF'; 120 | 121 | // Helper function to fetch the Container 122 | // from a public ID and service name provided 123 | async function getContainerFromPublicId(safeApp, pubName, subName) { 124 | console.log('getContainerFromPublicId(' + pubName + ', ' + subName + ')') 125 | let serviceInfo; 126 | let subNamesContainer; 127 | try { 128 | const address = await safeApp.crypto.sha3Hash(pubName); 129 | subNamesContainer = await safeApp.mutableData.newPublic(address, CONSTANTS.TYPE_TAG.DNS); 130 | serviceInfo = await subNamesContainer.get(subName || 'www'); // default it to www 131 | } catch (err) { 132 | switch (err.code) { 133 | case errConst.ERR_NO_SUCH_DATA.code: 134 | // there is no container stored at the location 135 | throw makeError(errConst.ERR_CONTENT_NOT_FOUND.code, errConst.ERR_CONTENT_NOT_FOUND.msg); 136 | case errConst.ERR_NO_SUCH_ENTRY.code: 137 | // Let's then try to read it as an RDF container 138 | return readPublicIdAsRdf(safeApp, subNamesContainer, pubName, subName); 139 | default: 140 | throw err; 141 | } 142 | } 143 | 144 | if (serviceInfo.buf.length === 0) { 145 | // the matching service name was soft-deleted 146 | throw makeError(errConst.ERR_SERVICE_NOT_FOUND.code, errConst.ERR_SERVICE_NOT_FOUND.msg); 147 | } 148 | 149 | let serviceMd; 150 | try { 151 | console.log('reading serviceInfo: ', serviceInfo) 152 | serviceMd = await this.mutableData.fromSerial(serviceInfo.buf); 153 | } catch (e) { 154 | console.log('creating serviceInfo: ', serviceInfo) 155 | serviceMd = await this.mutableData.newPublic(serviceInfo.buf, CONSTANTS.TYPE_TAG.WWW); 156 | } 157 | 158 | return { serviceMd, type: DATA_TYPE_NFS }; 159 | } 160 | 161 | // Helper function to fetch the Container 162 | // treating the public ID container as an RDF 163 | async function readPublicIdAsRdf(safeApp, subNamesContainer, pubName, subName) { 164 | console.log('readPublicIdAsRdf(' + subNamesContainer + ', ' + pubName + ', ' + subName + ')') 165 | let serviceMd; 166 | try { 167 | const graphId = `safe://${subName}.${pubName}`; 168 | const rdfEmulation = await subNamesContainer.emulateAs('RDF'); 169 | await rdfEmulation.nowOrWhenFetched([graphId]); 170 | const SAFETERMS = rdfEmulation.namespace('http://safenetwork.org/safevocab/'); 171 | let match = rdfEmulation.statementsMatching(rdfEmulation.sym(graphId), SAFETERMS('xorName'), undefined); 172 | const xorName = match[0].object.value.split(','); 173 | match = rdfEmulation.statementsMatching(rdfEmulation.sym(graphId), SAFETERMS('typeTag'), undefined); 174 | const typeTag = match[0].object.value; 175 | serviceMd = await safeApp.mutableData.newPublic(xorName, parseInt(typeTag, 10)); 176 | } catch (err) { 177 | // there is no matching subName name 178 | throw makeError(errConst.ERR_SERVICE_NOT_FOUND.code, errConst.ERR_SERVICE_NOT_FOUND.msg); 179 | } 180 | 181 | return { serviceMd, type: DATA_TYPE_RDF }; 182 | } 183 | 184 | module.exports = module.exports.WebIdProfile = WebIdProfile 185 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | /* NOTES: 4 | - When building for production I get "ModuleConcatenation bailout" messages because I'm not writing ES6 javascript, so at some point it may be worthwhile converting the code to ES6 in order to improve optimisation. 5 | 6 | - The 'bail' true and 'optimizationBailout' true settings don't alter the output, whereas I thought the first ModuleConcatenation bailout should halt webpack, and I should get more info on what caused the bailout. 7 | */ 8 | 9 | module.exports = { 10 | entry: './src/index-web.js', 11 | bail: true, 12 | stats: { 13 | // Examine all modules 14 | maxModules: Infinity, 15 | // Display bailout reasons 16 | optimizationBailout: true 17 | }, 18 | 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.js$/, 23 | exclude: /node_modules/ 24 | } 25 | ], 26 | }, 27 | externals: { 28 | 'node-fetch': 'fetch', 29 | 'solid-auth-cli': 'null', 30 | 'fs': 'null-fs' 31 | }, 32 | output: { 33 | path: path.resolve(__dirname, 'dist'), 34 | filename: 'safenetworkjs.js', 35 | library: 'Safenetworkjs', 36 | libraryTarget: 'umd' 37 | }, 38 | devtool: '#source-map', // #eval-source-map doesn't emit a map!? 39 | target: 'web', // default! 40 | }; 41 | --------------------------------------------------------------------------------