swarm-js
Version:
Swarm tools for JavaScript.
673 lines (575 loc) • 22.9 kB
JavaScript
// 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 = function (_ref) {
var fs = _ref.fs,
files = _ref.files,
os = _ref.os,
path = _ref.path,
child_process = _ref.child_process,
mimetype = _ref.mimetype,
defaultArchives = _ref.defaultArchives,
request = _ref.request,
downloadUrl = _ref.downloadUrl,
bytes = _ref.bytes,
hash = _ref.hash,
pick = _ref.pick;
// ∀ a . String -> JSON -> Map String a -o Map String a
// Inserts a key/val pair in an object impurely.
var impureInsert = function impureInsert(key) {
return function (val) {
return function (map) {
return map[key] = val, map;
};
};
}; // String -> JSON -> Map String JSON
// Merges an array of keys and an array of vals into an object.
var toMap = function toMap(keys) {
return function (vals) {
var map = {};
for (var 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.
var merge = function merge(a) {
return function (b) {
var map = {};
for (var key in a) {
map[key] = a[key];
}
for (var _key in b) {
map[_key] = b[_key];
}
return map;
};
}; // ∀ a . [a] -> [a] -> Bool
var equals = function equals(a) {
return function (b) {
if (a.length !== b.length) {
return false;
} else {
for (var i = 0, l = a.length; i < l; ++i) {
if (a[i] !== b[i]) return false;
}
}
return true;
};
}; // String -> String -> String
var rawUrl = function rawUrl(swarmUrl) {
return function (hash) {
return "".concat(swarmUrl, "/bzz-raw:/").concat(hash);
};
}; // String -> String -> Promise Uint8Array
// Gets the raw contents of a Swarm hash address.
var downloadData = function downloadData(swarmUrl) {
return function (hash) {
return new Promise(function (resolve, reject) {
request(rawUrl(swarmUrl)(hash), {
responseType: "arraybuffer"
}, function (err, arrayBuffer, response) {
if (err) {
return reject(err);
}
if (response.statusCode >= 400) {
return reject(new Error("Error ".concat(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.
var downloadEntries = function downloadEntries(swarmUrl) {
return function (hash) {
var search = function search(hash) {
return function (path) {
return function (routes) {
// Formats an entry to the Swarm.js type.
var format = function format(entry) {
return {
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
var downloadEntry = function 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(function (text) {
return JSON.parse(toString(text)).entries;
}).then(function (entries) {
return Promise.all(entries.map(downloadEntry));
}).then(function () {
return routes;
});
};
};
};
return search(hash)("")({});
};
}; // String -> String -> Promise (Map String String)
// Same as `downloadEntries`, but returns only hashes (no types).
var downloadRoutes = function downloadRoutes(swarmUrl) {
return function (hash) {
return downloadEntries(swarmUrl)(hash).then(function (entries) {
return toMap(Object.keys(entries))(Object.keys(entries).map(function (route) {
return 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.
var downloadDirectory = function downloadDirectory(swarmUrl) {
return function (hash) {
return downloadEntries(swarmUrl)(hash).then(function (entries) {
var paths = Object.keys(entries);
var hashs = paths.map(function (path) {
return entries[path].hash;
});
var types = paths.map(function (path) {
return entries[path].type;
});
var datas = hashs.map(downloadData(swarmUrl));
var files = function files(datas) {
return datas.map(function (data, i) {
return {
type: types[i],
data: data
};
});
};
return Promise.all(datas).then(function (datas) {
return 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.
var downloadDataToDisk = function downloadDataToDisk(swarmUrl) {
return function (hash) {
return function (filePath) {
return 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.
var downloadDirectoryToDisk = function downloadDirectoryToDisk(swarmUrl) {
return function (hash) {
return function (dirPath) {
return downloadRoutes(swarmUrl)(hash).then(function (routingTable) {
var downloads = [];
for (var route in routingTable) {
if (route.length > 0) {
var filePath = path.join(dirPath, route);
downloads.push(downloadDataToDisk(swarmUrl)(routingTable[route])(filePath));
}
;
}
;
return Promise.all(downloads).then(function () {
return dirPath;
});
});
};
};
}; // String -> Uint8Array -> Promise String
// Uploads raw data to Swarm.
// Returns a promise with the uploaded hash.
var uploadData = function uploadData(swarmUrl) {
return function (data) {
return new Promise(function (resolve, reject) {
var params = {
body: typeof data === "string" ? fromString(data) : data,
method: "POST"
};
request("".concat(swarmUrl, "/bzz-raw:/"), params, function (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?
var uploadToManifest = function uploadToManifest(swarmUrl) {
return function (hash) {
return function (route) {
return function (file) {
var attempt = function attempt(n) {
var slashRoute = route[0] === "/" ? route : "/" + route;
var url = "".concat(swarmUrl, "/bzz:/").concat(hash).concat(slashRoute);
var opt = {
method: "PUT",
headers: {
"Content-Type": file.type
},
body: file.data
};
return new Promise(function (resolve, reject) {
request(url, opt, function (err, data) {
if (err) {
return reject(err);
}
if (data.indexOf("error") !== -1) {
return reject(data);
}
return resolve(data);
});
})["catch"](function (e) {
return n > 0 && attempt(n - 1);
});
};
return attempt(3);
};
};
};
}; // String -> {type: String, data: Uint8Array} -> Promise String
var uploadFile = function uploadFile(swarmUrl) {
return function (file) {
return uploadDirectory(swarmUrl)({
"": file
});
};
}; // String -> String -> Promise String
var uploadFileFromDisk = function uploadFileFromDisk(swarmUrl) {
return function (filePath) {
return fs.readFile(filePath).then(function (data) {
return 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.
var uploadDirectory = function uploadDirectory(swarmUrl) {
return function (directory) {
return uploadData(swarmUrl)("{}").then(function (hash) {
var uploadRoute = function uploadRoute(route) {
return function (hash) {
return uploadToManifest(swarmUrl)(hash)(route)(directory[route]);
};
};
var uploadToHash = function uploadToHash(hash, route) {
return hash.then(uploadRoute(route));
};
return Object.keys(directory).reduce(uploadToHash, Promise.resolve(hash));
});
};
}; // String -> Promise String
var uploadDataFromDisk = function uploadDataFromDisk(swarmUrl) {
return function (filePath) {
return fs.readFile(filePath).then(uploadData(swarmUrl));
};
}; // String -> Nullable String -> String -> Promise String
var uploadDirectoryFromDisk = function uploadDirectoryFromDisk(swarmUrl) {
return function (defaultPath) {
return function (dirPath) {
return files.directoryTree(dirPath).then(function (fullPaths) {
return Promise.all(fullPaths.map(function (path) {
return fs.readFile(path);
})).then(function (datas) {
var paths = fullPaths.map(function (path) {
return path.slice(dirPath.length);
});
var types = fullPaths.map(function (path) {
return mimetype.lookup(path) || "text/plain";
});
return toMap(paths)(datas.map(function (data, i) {
return {
type: types[i],
data: data
};
}));
});
}).then(function (directory) {
return 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.
var _upload = function upload(swarmUrl) {
return function (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.
var _download = function download(swarmUrl) {
return function (hash) {
return function (path) {
return isDirectory(swarmUrl)(hash).then(function (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.
var downloadBinary = function downloadBinary(path, archives) {
var system = os.platform().replace("win32", "windows") + "-" + (os.arch() === "x64" ? "amd64" : "386");
var archive = (archives || defaultArchives)[system];
var archiveUrl = downloadUrl + archive.archive + ".tar.gz";
var archiveMD5 = archive.archiveMD5;
var 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.
var startProcess = function startProcess(swarmSetup) {
return new Promise(function (resolve, reject) {
var spawn = child_process.spawn;
var hasString = function hasString(str) {
return function (buffer) {
return ('' + buffer).indexOf(str) !== -1;
};
};
var account = swarmSetup.account,
password = swarmSetup.password,
dataDir = swarmSetup.dataDir,
ensApi = swarmSetup.ensApi,
privateKey = swarmSetup.privateKey;
var STARTUP_TIMEOUT_SECS = 3;
var WAITING_PASSWORD = 0;
var STARTING = 1;
var LISTENING = 2;
var PASSWORD_PROMPT_HOOK = "Passphrase";
var LISTENING_HOOK = "Swarm http proxy started";
var state = WAITING_PASSWORD;
var swarmProcess = spawn(swarmSetup.binPath, ['--bzzaccount', account || privateKey, '--datadir', dataDir, '--ens-api', ensApi]);
var handleProcessOutput = function handleProcessOutput(data) {
if (state === WAITING_PASSWORD && hasString(PASSWORD_PROMPT_HOOK)(data)) {
setTimeout(function () {
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));
var restart = function restart() {
return startProcess(swarmSetup).then(resolve)["catch"](reject);
};
var error = function error() {
return reject(new Error("Couldn't start swarm process."));
};
var timeout = setTimeout(error, 20000);
});
}; // Process ~> Promise ()
// Stops the Swarm process.
var stopProcess = function stopProcess(process) {
return new Promise(function (resolve, reject) {
process.stderr.removeAllListeners('data');
process.stdout.removeAllListeners('data');
process.stdin.removeAllListeners('error');
process.removeAllListeners('error');
process.removeAllListeners('exit');
process.kill('SIGINT');
var killTimeout = setTimeout(function () {
return process.kill('SIGKILL');
}, 8000);
process.once('close', function () {
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`)
var local = function local(swarmSetup) {
return function (useAPI) {
return _isAvailable("http://localhost:8500").then(function (isAvailable) {
return isAvailable ? useAPI(at("http://localhost:8500")).then(function () {}) : downloadBinary(swarmSetup.binPath, swarmSetup.archives).onData(function (data) {
return (swarmSetup.onProgress || function () {})(data.length);
}).then(function () {
return startProcess(swarmSetup);
}).then(function (process) {
return useAPI(at("http://localhost:8500")).then(function () {
return process;
});
}).then(stopProcess);
});
};
}; // String ~> Promise Bool
// Returns true if Swarm is available on `url`.
// Perfoms a test upload to determine that.
// TODO: improve this?
var _isAvailable = function isAvailable(swarmUrl) {
var testFile = "test";
var testHash = "c9a99c7d326dcc6316f32fe2625b311f6dc49a175e6877681ded93137d3569e7";
return uploadData(swarmUrl)(testFile).then(function (hash) {
return hash === testHash;
})["catch"](function () {
return 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?
var isDirectory = function isDirectory(swarmUrl) {
return function (hash) {
return downloadData(swarmUrl)(hash).then(function (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.
var uncurry = function uncurry(f) {
return function (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?
var test = function test() {
return Promise.resolve(true);
}; // Uint8Array -> String
var toString = function toString(uint8Array) {
return bytes.toString(bytes.fromUint8Array(uint8Array));
}; // String -> Uint8Array
var fromString = function fromString(string) {
return bytes.toUint8Array(bytes.fromString(string));
}; // String -> SwarmAPI
// Fixes the `swarmUrl`, returning an API where you don't have to pass it.
var at = function at(swarmUrl) {
return {
download: function download(hash, path) {
return _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: function isAvailable() {
return _isAvailable(swarmUrl);
},
upload: function upload(arg) {
return _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: at,
local: local,
download: _download,
downloadBinary: downloadBinary,
downloadData: downloadData,
downloadDataToDisk: downloadDataToDisk,
downloadDirectory: downloadDirectory,
downloadDirectoryToDisk: downloadDirectoryToDisk,
downloadEntries: downloadEntries,
downloadRoutes: downloadRoutes,
isAvailable: _isAvailable,
startProcess: startProcess,
stopProcess: stopProcess,
upload: _upload,
uploadData: uploadData,
uploadDataFromDisk: uploadDataFromDisk,
uploadFile: uploadFile,
uploadFileFromDisk: uploadFileFromDisk,
uploadDirectory: uploadDirectory,
uploadDirectoryFromDisk: uploadDirectoryFromDisk,
uploadToManifest: uploadToManifest,
pick: pick,
hash: hash,
fromString: fromString,
toString: toString
};
};