Files
Superalgos/Client/node_modules/swarm-js/src/swarm.js
T

506 lines
18 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// TODO: this is a temporary fix to hide those libraries from the browser. A
// slightly better long-term solution would be to split this file into two,
// separating the functions that are used on Node.js from the functions that
// are used only on the browser.
module.exports = ({
fs,
files,
os,
path,
child_process,
mimetype,
defaultArchives,
request,
downloadUrl,
bytes,
hash,
pick
}) => {
// ∀ a . String -> JSON -> Map String a -o Map String a
// Inserts a key/val pair in an object impurely.
const impureInsert = key => val => map =>
(map[key] = val, map);
// String -> JSON -> Map String JSON
// Merges an array of keys and an array of vals into an object.
const toMap = keys => vals => {
let map = {};
for (let i = 0, l = keys.length; i < l; ++i)
map[keys[i]] = vals[i];
return map;
};
// ∀ a . Map String a -> Map String a -> Map String a
// Merges two maps into one.
const merge = a => b => {
let map = {};
for (let key in a)
map[key] = a[key];
for (let key in b)
map[key] = b[key];
return map;
};
// ∀ a . [a] -> [a] -> Bool
const equals = a => b => {
if (a.length !== b.length) {
return false;
} else {
for (let i = 0, l = a.length; i < l; ++i) {
if (a[i] !== b[i]) return false;
}
}
return true;
}
// String -> String -> String
const rawUrl = swarmUrl => hash =>
`${swarmUrl}/bzz-raw:/${hash}`
// String -> String -> Promise Uint8Array
// Gets the raw contents of a Swarm hash address.
const downloadData = swarmUrl => hash =>
new Promise((resolve, reject) => {
request(rawUrl(swarmUrl)(hash), {responseType: "arraybuffer"}, (err, arrayBuffer, response) => {
if (err) {
return reject(err);
}
if (response.statusCode >= 400) {
return reject(new Error(`Error ${response.statusCode}.`));
}
return resolve(new Uint8Array(arrayBuffer));
})
});
// type Entry = {"type": String, "hash": String}
// type File = {"type": String, "data": Uint8Array}
// String -> String -> Promise (Map String Entry)
// Solves the manifest of a Swarm address recursively.
// Returns a map from full paths to entries.
const downloadEntries = swarmUrl => hash => {
const search = hash => path => routes => {
// Formats an entry to the Swarm.js type.
const format = entry => ({
type: entry.contentType,
hash: entry.hash});
// To download a single entry:
// if type is bzz-manifest, go deeper
// if not, add it to the routing table
const downloadEntry = entry => {
if (entry.path === undefined) {
return Promise.resolve();
} else {
return entry.contentType === "application/bzz-manifest+json"
? search (entry.hash) (path + entry.path) (routes)
: Promise.resolve (impureInsert (path + entry.path) (format(entry)) (routes));
}
}
// Downloads the initial manifest and then each entry.
return downloadData(swarmUrl)(hash)
.then(text => JSON.parse(toString(text)).entries)
.then(entries => Promise.all(entries.map(downloadEntry)))
.then(() => routes);
}
return search (hash) ("") ({});
}
// String -> String -> Promise (Map String String)
// Same as `downloadEntries`, but returns only hashes (no types).
const downloadRoutes = swarmUrl => hash =>
downloadEntries(swarmUrl)(hash)
.then(entries => toMap
(Object.keys(entries))
(Object.keys(entries).map(route => entries[route].hash)));
// String -> String -> Promise (Map String File)
// Gets the entire directory tree in a Swarm address.
// Returns a promise mapping paths to file contents.
const downloadDirectory = swarmUrl => hash =>
downloadEntries (swarmUrl) (hash)
.then(entries => {
const paths = Object.keys(entries);
const hashs = paths.map(path => entries[path].hash);
const types = paths.map(path => entries[path].type);
const datas = hashs.map(downloadData(swarmUrl));
const files = datas => datas.map((data, i) => ({type: types[i], data: data}));
return Promise.all(datas).then(datas => toMap(paths)(files(datas)));
});
// String -> String -> String -> Promise String
// Gets the raw contents of a Swarm hash address.
// Returns a promise with the downloaded file path.
const downloadDataToDisk = swarmUrl => hash => filePath =>
files.download (rawUrl(swarmUrl)(hash)) (filePath);
// String -> String -> String -> Promise (Map String String)
// Gets the entire directory tree in a Swarm address.
// Returns a promise mapping paths to file contents.
const downloadDirectoryToDisk = swarmUrl => hash => dirPath =>
downloadRoutes (swarmUrl) (hash)
.then(routingTable => {
let downloads = [];
for (let route in routingTable) {
if (route.length > 0) {
const filePath = path.join(dirPath, route);
downloads.push(downloadDataToDisk(swarmUrl)(routingTable[route])(filePath));
};
};
return Promise.all(downloads).then(() => dirPath);
});
// String -> Uint8Array -> Promise String
// Uploads raw data to Swarm.
// Returns a promise with the uploaded hash.
const uploadData = swarmUrl => data =>
new Promise((resolve, reject) => {
const params = {
body: typeof data === "string" ? fromString(data) : data,
method: "POST"
};
request(`${swarmUrl}/bzz-raw:/`, params, (err, data) => {
if (err) {
return reject(err);
}
return resolve(data);
});
});
// String -> String -> String -> File -> Promise String
// Uploads a file to the Swarm manifest at a given hash, under a specific
// route. Returns a promise containing the uploaded hash.
// FIXME: for some reasons Swarm-Gateways is sometimes returning
// error 404 (bad request), so we retry up to 3 times. Why?
const uploadToManifest = swarmUrl => hash => route => file => {
const attempt = n => {
const slashRoute = route[0] === "/" ? route : "/" + route;
const url = `${swarmUrl}/bzz:/${hash}${slashRoute}`;
const opt = {
method: "PUT",
headers: {"Content-Type": file.type},
body: file.data};
return new Promise((resolve, reject) => {
request(url, opt, (err, data) => {
if (err) {
return reject(err);
}
if (data.indexOf("error") !== -1) {
return reject(data);
}
return resolve(data);
});
}).catch(e => n > 0 && attempt(n-1));
};
return attempt(3);
};
// String -> {type: String, data: Uint8Array} -> Promise String
const uploadFile = swarmUrl => file =>
uploadDirectory(swarmUrl)({"": file});
// String -> String -> Promise String
const uploadFileFromDisk = swarmUrl => filePath =>
fs.readFile(filePath)
.then(data => uploadFile(swarmUrl)({type: mimetype.lookup(filePath), data: data}));
// String -> Map String File -> Promise String
// Uploads a directory to Swarm. The directory is
// represented as a map of routes and files.
// A default path is encoded by having a "" route.
const uploadDirectory = swarmUrl => directory =>
uploadData(swarmUrl)("{}")
.then(hash => {
const uploadRoute = route => hash => uploadToManifest(swarmUrl)(hash)(route)(directory[route]);
const uploadToHash = (hash, route) => hash.then(uploadRoute(route));
return Object.keys(directory).reduce(uploadToHash, Promise.resolve(hash));
});
// String -> Promise String
const uploadDataFromDisk = swarmUrl => filePath =>
fs.readFile(filePath)
.then(uploadData(swarmUrl));
// String -> Nullable String -> String -> Promise String
const uploadDirectoryFromDisk = swarmUrl => defaultPath => dirPath =>
files.directoryTree(dirPath)
.then(fullPaths => Promise.all(fullPaths.map(path => fs.readFile(path))).then(datas => {
const paths = fullPaths.map(path => path.slice(dirPath.length));
const types = fullPaths.map(path => mimetype.lookup(path) || "text/plain");
return toMap (paths) (datas.map((data, i) => ({type: types[i], data: data})));
}))
.then(directory => merge (defaultPath ? {"": directory[defaultPath]} : {}) (directory))
.then(uploadDirectory(swarmUrl));
// String -> UploadInfo -> Promise String
// Simplified multi-type upload which calls the correct
// one based on the type of the argument given.
const upload = swarmUrl => arg => {
// Upload raw data from browser
if (arg.pick === "data") {
return pick.data().then(uploadData(swarmUrl));
// Upload a file from browser
} else if (arg.pick === "file") {
return pick.file().then(uploadFile(swarmUrl));
// Upload a directory from browser
} else if (arg.pick === "directory") {
return pick.directory().then(uploadDirectory(swarmUrl));
// Upload directory/file from disk
} else if (arg.path) {
switch (arg.kind) {
case "data": return uploadDataFromDisk(swarmUrl)(arg.path);
case "file": return uploadFileFromDisk(swarmUrl)(arg.path);
case "directory": return uploadDirectoryFromDisk(swarmUrl)(arg.defaultFile)(arg.path);
};
// Upload UTF-8 string or raw data (buffer)
} else if (arg.length || typeof arg === "string") {
return uploadData(swarmUrl)(arg);
// Upload directory with JSON
} else if (arg instanceof Object) {
return uploadDirectory(swarmUrl)(arg);
}
return Promise.reject(new Error("Bad arguments"));
}
// String -> String -> Nullable String -> Promise (String | Uint8Array | Map String Uint8Array)
// Simplified multi-type download which calls the correct function based on
// the type of the argument given, and on whether the Swwarm address has a
// directory or a file.
const download = swarmUrl => hash => path =>
isDirectory(swarmUrl)(hash).then(isDir => {
if (isDir) {
return path
? downloadDirectoryToDisk(swarmUrl)(hash)(path)
: downloadDirectory(swarmUrl)(hash);
} else {
return path
? downloadDataToDisk(swarmUrl)(hash)(path)
: downloadData(swarmUrl)(hash);
}
});
// String -> Promise String
// Downloads the Swarm binaries into a path. Returns a promise that only
// resolves when the exact Swarm file is there, and verified to be correct.
// If it was already there to begin with, skips the download.
const downloadBinary = (path, archives) => {
const system = os.platform().replace("win32","windows") + "-" + (os.arch() === "x64" ? "amd64" : "386");
const archive = (archives || defaultArchives)[system];
const archiveUrl = downloadUrl + archive.archive + ".tar.gz";
const archiveMD5 = archive.archiveMD5;
const binaryMD5 = archive.binaryMD5;
return files.safeDownloadArchived(archiveUrl)(archiveMD5)(binaryMD5)(path);
};
// type SwarmSetup = {
// account : String,
// password : String,
// dataDir : String,
// binPath : String,
// ensApi : String,
// onDownloadProgress : Number ~> (),
// archives : [{
// archive: String,
// binaryMD5: String,
// archiveMD5: String
// }]
// }
// SwarmSetup ~> Promise Process
// Starts the Swarm process.
const startProcess = swarmSetup => new Promise((resolve, reject) => {
const {spawn} = child_process;
const hasString = str => buffer => ('' + buffer).indexOf(str) !== -1;
const {account, password, dataDir, ensApi, privateKey} = swarmSetup;
const STARTUP_TIMEOUT_SECS = 3;
const WAITING_PASSWORD = 0;
const STARTING = 1;
const LISTENING = 2;
const PASSWORD_PROMPT_HOOK = "Passphrase";
const LISTENING_HOOK = "Swarm http proxy started";
let state = WAITING_PASSWORD;
const swarmProcess = spawn(swarmSetup.binPath, [
'--bzzaccount', account || privateKey,
'--datadir', dataDir,
'--ens-api', ensApi]);
const handleProcessOutput = data => {
if (state === WAITING_PASSWORD && hasString (PASSWORD_PROMPT_HOOK) (data)) {
setTimeout(() => {
state = STARTING;
swarmProcess.stdin.write(password + '\n');
}, 500);
} else if (hasString (LISTENING_HOOK) (data)) {
state = LISTENING;
clearTimeout(timeout);
resolve(swarmProcess);
}
}
swarmProcess.stdout.on('data', handleProcessOutput);
swarmProcess.stderr.on('data', handleProcessOutput);
//swarmProcess.on('close', () => setTimeout(restart, 2000));
let restart = () => startProcess(swarmSetup).then(resolve).catch(reject);
let error = () => reject(new Error("Couldn't start swarm process."));
let timeout = setTimeout(error, 20000);
});
// Process ~> Promise ()
// Stops the Swarm process.
const stopProcess = process => new Promise((resolve, reject) => {
process.stderr.removeAllListeners('data');
process.stdout.removeAllListeners('data');
process.stdin.removeAllListeners('error');
process.removeAllListeners('error');
process.removeAllListeners('exit');
process.kill('SIGINT');
const killTimeout = setTimeout(
() => process.kill('SIGKILL'),
8000);
process.once('close', () => {
clearTimeout(killTimeout);
resolve();
});
});
// SwarmSetup -> (SwarmAPI -> Promise ()) -> Promise ()
// Receives a Swarm configuration object and a callback function. It then
// checks if a local Swarm node is running. If no local Swarm is found, it
// downloads the Swarm binaries to the dataDir (if not there), checksums,
// starts the Swarm process and calls the callback function with an API
// object using the local node. That callback must return a promise which
// will resolve when it is done using the API, so that this function can
// close the Swarm process properly. Returns a promise that resolves when the
// user is done with the API and the Swarm process is closed.
// TODO: check if Swarm process is already running (improve `isAvailable`)
const local = swarmSetup => useAPI =>
isAvailable("http://localhost:8500").then(isAvailable =>
isAvailable
? useAPI(at("http://localhost:8500")).then(() => {})
: downloadBinary(swarmSetup.binPath, swarmSetup.archives)
.onData(data => (swarmSetup.onProgress || (() => {}))(data.length))
.then(() => startProcess(swarmSetup))
.then(process => useAPI(at("http://localhost:8500")).then(() => process))
.then(stopProcess));
// String ~> Promise Bool
// Returns true if Swarm is available on `url`.
// Perfoms a test upload to determine that.
// TODO: improve this?
const isAvailable = swarmUrl => {
const testFile = "test";
const testHash = "c9a99c7d326dcc6316f32fe2625b311f6dc49a175e6877681ded93137d3569e7";
return uploadData(swarmUrl)(testFile)
.then(hash => hash === testHash)
.catch(() => false);
};
// String -> String ~> Promise Bool
// Returns a Promise which is true if that Swarm address is a directory.
// Determines that by checking that it (i) is a JSON, (ii) has a .entries.
// TODO: improve this?
const isDirectory = swarmUrl => hash =>
downloadData(swarmUrl)(hash)
.then(data => {
try {
return !!JSON.parse(toString(data)).entries;
} catch (e) {
return false;
}
});
// Uncurries a function; used to allow the f(x,y,z) style on exports.
const uncurry = f => (a,b,c,d,e) => {
var p;
// Hardcoded because efficiency (`arguments` is very slow).
if (typeof a !== "undefined") p = f(a);
if (typeof b !== "undefined") p = f(b);
if (typeof c !== "undefined") p = f(c);
if (typeof d !== "undefined") p = f(d);
if (typeof e !== "undefined") p = f(e);
return p;
};
// () -> Promise Bool
// Not sure how to mock Swarm to test it properly. Ideas?
const test = () => Promise.resolve(true);
// Uint8Array -> String
const toString = uint8Array =>
bytes.toString(bytes.fromUint8Array(uint8Array));
// String -> Uint8Array
const fromString = string =>
bytes.toUint8Array(bytes.fromString(string));
// String -> SwarmAPI
// Fixes the `swarmUrl`, returning an API where you don't have to pass it.
const at = swarmUrl => ({
download: (hash,path) => download(swarmUrl)(hash)(path),
downloadData: uncurry(downloadData(swarmUrl)),
downloadDataToDisk: uncurry(downloadDataToDisk(swarmUrl)),
downloadDirectory: uncurry(downloadDirectory(swarmUrl)),
downloadDirectoryToDisk: uncurry(downloadDirectoryToDisk(swarmUrl)),
downloadEntries: uncurry(downloadEntries(swarmUrl)),
downloadRoutes: uncurry(downloadRoutes(swarmUrl)),
isAvailable: () => isAvailable(swarmUrl),
upload: (arg) => upload(swarmUrl)(arg),
uploadData: uncurry(uploadData(swarmUrl)),
uploadFile: uncurry(uploadFile(swarmUrl)),
uploadFileFromDisk: uncurry(uploadFile(swarmUrl)),
uploadDataFromDisk: uncurry(uploadDataFromDisk(swarmUrl)),
uploadDirectory: uncurry(uploadDirectory(swarmUrl)),
uploadDirectoryFromDisk: uncurry(uploadDirectoryFromDisk(swarmUrl)),
uploadToManifest: uncurry(uploadToManifest(swarmUrl)),
pick: pick,
hash: hash,
fromString: fromString,
toString: toString
});
return {
at,
local,
download,
downloadBinary,
downloadData,
downloadDataToDisk,
downloadDirectory,
downloadDirectoryToDisk,
downloadEntries,
downloadRoutes,
isAvailable,
startProcess,
stopProcess,
upload,
uploadData,
uploadDataFromDisk,
uploadFile,
uploadFileFromDisk,
uploadDirectory,
uploadDirectoryFromDisk,
uploadToManifest,
pick,
hash,
fromString,
toString
};
};