Beta 7 with some repo names changed.
This commit is contained in:
1 parent
0f7b40cca7
commit
963c19493a
3408 files changed
+462217
-15516
No files matched your search
+30
@@ -0,0 +1,30 @@
|
||||
const unavailable = () => { throw "This swarm.js function isn't available on the browser."; }
|
||||
|
||||
const fs = {readFile: unavailable};
|
||||
const files = {download: unavailable, safeDownloadArchived: unavailable, directoryTree: unavailable};
|
||||
const os = {platform: unavailable, arch: unavailable};
|
||||
const path = {join: unavailable, slice: unavailable};
|
||||
const child_process = {spawn: unavailable};
|
||||
const mimetype = {lookup: unavailable};
|
||||
const defaultArchives = {};
|
||||
const downloadUrl = null;
|
||||
const request = require("xhr-request");
|
||||
const bytes = require("eth-lib/lib/bytes");
|
||||
const hash = require("./swarm-hash.js");
|
||||
const pick = require("./pick.js");
|
||||
const swarm = require("./swarm");
|
||||
|
||||
module.exports = swarm({
|
||||
fs,
|
||||
files,
|
||||
os,
|
||||
path,
|
||||
child_process,
|
||||
defaultArchives,
|
||||
mimetype,
|
||||
request,
|
||||
downloadUrl,
|
||||
bytes,
|
||||
hash,
|
||||
pick
|
||||
});
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
const fs = require("fs-extra");
|
||||
const files = require("./files.js");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const child_process = require("child_process");
|
||||
const mimetype = require('mime-types');
|
||||
const defaultArchives = require("./../archives/archives.json");
|
||||
const requester = require("xhr-request");
|
||||
const downloadUrl = "http://ethereum-mist.s3.amazonaws.com/swarm/";
|
||||
const bytes = require("eth-lib/lib/bytes");
|
||||
const hash = require("./swarm-hash.js");
|
||||
const pick = require("./pick.js");
|
||||
const swarm = require("./swarm");
|
||||
|
||||
// Fixes issue that causes xhr-request-promise on Node.js to only accept Buffer
|
||||
const request = (url, params, callback) => {
|
||||
let newParams = {};
|
||||
for (let key in params) {
|
||||
newParams[key] = params[key];
|
||||
}
|
||||
if (typeof newParams.body !== "undefined") {
|
||||
newParams.body = newParams.body instanceof Buffer
|
||||
? newParams.body
|
||||
: new Buffer(newParams.body);
|
||||
}
|
||||
return requester(url, newParams, callback);
|
||||
};
|
||||
|
||||
module.exports = swarm({
|
||||
fs,
|
||||
files,
|
||||
os,
|
||||
path,
|
||||
child_process,
|
||||
defaultArchives,
|
||||
mimetype,
|
||||
request,
|
||||
downloadUrl,
|
||||
bytes,
|
||||
hash,
|
||||
pick
|
||||
});
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
// This module implements some file download utils. Its most important export
|
||||
// is `safeDownloadTargzFile`, which, given a file, its md5, a tar.gz url, its
|
||||
// md5 and a path, returns a Promise that will only resolve once the exact file
|
||||
// you expect is available on that path.
|
||||
|
||||
const Q = require("bluebird");
|
||||
const assert = require("assert");
|
||||
const crypto = require("crypto");
|
||||
const fs = require("fs-extra");
|
||||
const got = require("got");
|
||||
const mkdirp = require("mkdirp-promise");
|
||||
const path = require("path");
|
||||
const tar = require("tar");
|
||||
|
||||
// String -> String ~> Promise String
|
||||
// Downloads a file from an url to a path.
|
||||
// Returns a promise containing the path.
|
||||
const download = url => filePath => {
|
||||
const promise = Q.resolve(mkdirp(path.dirname(filePath))).then(() =>
|
||||
new Q((resolve, reject) => {
|
||||
const writeStream = fs.createWriteStream(filePath);
|
||||
const downloadStream = got.stream(url);
|
||||
downloadStream.on("end", () => resolve(filePath));
|
||||
downloadStream.on("data", chunk => promise.onDataCallback(chunk));
|
||||
downloadStream.on("error", reject);
|
||||
downloadStream.pipe(writeStream);
|
||||
}));
|
||||
promise.onDataCallback = () => {};
|
||||
promise.onData = callback => {
|
||||
promise.onDataCallback = callback || (() => {});
|
||||
return promise;
|
||||
}
|
||||
return promise;
|
||||
};
|
||||
|
||||
// String -> String ~> Promise String
|
||||
// Hashes a file using the given algorithm (ex: "md5").
|
||||
// Returns a promise containing the hashed string.
|
||||
const hash = algorithm => path =>
|
||||
new Q((resolve, reject) => {
|
||||
const readStream = fs.ReadStream(path);
|
||||
const hash = crypto.createHash(algorithm);
|
||||
readStream.on("data", d => hash.update(d));
|
||||
readStream.on("end", () => resolve(hash.digest("hex")));
|
||||
readStream.on("error", reject);
|
||||
});
|
||||
|
||||
// String -> String ~> Promise ()
|
||||
// Asserts a file matches this md5 hash.
|
||||
// Returns a promise containing its path.
|
||||
const checksum = fileHash => path =>
|
||||
hash("md5")(path)
|
||||
.then(actualHash => actualHash === fileHash)
|
||||
.then(assert)
|
||||
.then(() => path);
|
||||
|
||||
// String ~> String ~> String ~> Promise String
|
||||
// Downloads a file to a directory, check.
|
||||
// Checks if the md5 hash matches.
|
||||
// Returns a promise containing the path.
|
||||
const downloadAndCheck = url => path => fileHash =>
|
||||
download(url)(path)
|
||||
.then(checksum(fileHash));
|
||||
|
||||
// String -> String ~> Promise String
|
||||
// TODO: work for zip and other types
|
||||
const extract = fromPath => toPath =>
|
||||
tar.x(fromPath, toPath)
|
||||
.then(() => toPath);
|
||||
|
||||
// String ~> Promise String
|
||||
// Reads a file as an UTF8 string.
|
||||
// Returns a promise containing that string.
|
||||
const readUTF8 = path =>
|
||||
fs.readFile (path, {encoding: "utf8"});
|
||||
|
||||
// String ~> Promise Bool
|
||||
const isDirectory = path =>
|
||||
fs.exists(path)
|
||||
.then(assert)
|
||||
.then(() => fs.lstat(path))
|
||||
.then(stats => stats.isDirectory())
|
||||
.catch(() => false);
|
||||
|
||||
// String -> Promise String
|
||||
const directoryTree = dirPath => {
|
||||
let paths = [];
|
||||
const search = dirPath =>
|
||||
isDirectory(dirPath).then(isDir => {
|
||||
if (isDir) {
|
||||
const searchOnDir = dir => search (path.join(dirPath, dir));
|
||||
return Q.all(Q.map(fs.readdir(dirPath), searchOnDir));
|
||||
} else {
|
||||
paths.push(dirPath);
|
||||
};
|
||||
});
|
||||
return Q.all(search (dirPath)).then(() => paths);
|
||||
}
|
||||
|
||||
// Regex -> String ~> Promise (Array String)
|
||||
const search = regex => dirPath =>
|
||||
directoryTree(dirPath)
|
||||
.then(tree => tree.filter(path => regex.test(path)));
|
||||
|
||||
// String -> String -> String -> String ~> Promise String
|
||||
// Downloads a file inside a tar.gz and places it at `filePath`.
|
||||
// Checks the md5 hash of the tar before extracting it.
|
||||
// Checks the md5 hash of the file after extracting it.
|
||||
// If all is OK, returns a promise containing the file path.
|
||||
const safeDownloadArchived = url => archiveHash => fileHash => filePath => {
|
||||
const fileDir = path.dirname(filePath);
|
||||
const fileName = path.basename(filePath);
|
||||
const archivePath = path.join(fileDir, ".swarm_downloads/files.tar.gz");
|
||||
const archiveDir = path.dirname(archivePath);
|
||||
const promise = Q.resolve(mkdirp(archiveDir))
|
||||
.then(() => checksum (fileHash) (filePath))
|
||||
.then(() => filePath)
|
||||
.catch(() => fs.exists(archiveDir)
|
||||
.then(exists => !exists ? fs.mkdir(archiveDir) : null)
|
||||
.then(() => download (url)(archivePath).onData(promise.onDataCallback))
|
||||
.then(() => hash("md5")(archivePath))
|
||||
.then(() => archiveHash ? checksum(archiveHash)(archivePath) : null)
|
||||
.then(() => extract (archivePath) (archiveDir))
|
||||
.then(() => search (new RegExp(fileName+"$")) (archiveDir))
|
||||
.then(fp => fs.rename (fp[0], filePath))
|
||||
.then(() => fs.unlink (archivePath))
|
||||
.then(() => fileHash ? checksum(fileHash)(filePath) : null)
|
||||
.then(() => filePath));
|
||||
promise.onDataCallback = () => {};
|
||||
promise.onData = callback => {
|
||||
promise.onDataCallback = callback || (() => {});
|
||||
return promise;
|
||||
};
|
||||
return promise;
|
||||
};
|
||||
|
||||
// String -> String ~> Promise String
|
||||
// Like `safeDownloadArchivedFile`, but without the checksums.
|
||||
const downloadArchived = url => path =>
|
||||
safeDownloadArchived (url) (null) (null) (path);
|
||||
|
||||
// () => Promise Bool
|
||||
// Tests the implementation by downloading a predetermined tar.gz
|
||||
// from a mocked HTTP-server into a mocked filesystem. Does some
|
||||
// redundancy tests such as checking the file constents and double
|
||||
// checking its MD5 hash.
|
||||
// Returns a promise containing a boolean, true if tests passed.
|
||||
const test = () => {
|
||||
const filePath = "/swarm/foo.txt";
|
||||
const fileHash = "d3b07384d113edec49eaa6238ad5ff00";
|
||||
const archiveUrl = "http://localhost:12534";
|
||||
const archiveHash = "7fa45f946bb2a696bdd9972e0fbceac2";
|
||||
const archiveData = new Buffer([
|
||||
0x1f,0x8b,0x08,0x00,0xf1,0x34,0xaf,0x58,0x00,0x03,0xed,0xcf,0x3d,0x0e,0x83,0x30,
|
||||
0x0c,0x86,0x61,0x66,0x4e,0xe1,0x13,0x54,0xce,0x0f,0xc9,0x79,0x58,0xb2,0x46,0x82,
|
||||
0x14,0x71,0x7c,0xd2,0x06,0x31,0x52,0x75,0x40,0x08,0xe9,0x7d,0x96,0x4f,0x96,0x3d,
|
||||
0x7c,0x4e,0x39,0xbf,0xca,0x5a,0xba,0x2b,0xa9,0x6a,0xf0,0x5e,0x3e,0x19,0xc3,0xf0,
|
||||
0x4d,0xb5,0x6d,0xde,0x79,0x31,0x4e,0x07,0x17,0x9c,0xb5,0x31,0x8a,0x1a,0xab,0xc6,
|
||||
0x77,0xa2,0x97,0xb6,0xda,0xbd,0xe7,0x32,0x4e,0xb5,0xca,0xf2,0xe3,0xae,0x9e,0xa5,
|
||||
0x74,0xb2,0x6f,0x8f,0xc8,0x91,0x0f,0x91,0x72,0xee,0xef,0xee,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xf8,0xdf,0x06,0xb3,0x2a,0xda,
|
||||
0xed,0x00,0x28,0x00,0x00]);
|
||||
|
||||
const crypto = require("crypto");
|
||||
const fsMock = require("mock-fs")({"/swarm":{}});
|
||||
const httpMock = require("http")
|
||||
.createServer((_,res) => res.end(archiveData))
|
||||
.listen(12534);
|
||||
|
||||
return safeDownloadArchived (archiveUrl) (archiveHash) (fileHash) (filePath)
|
||||
.then(checksum (fileHash))
|
||||
.then(readUTF8)
|
||||
.then(text => text === "foo\n")
|
||||
.then(assert)
|
||||
.then(() => safeDownloadArchived (archiveUrl) (archiveHash) (fileHash) (filePath))
|
||||
.then(() => true)
|
||||
.catch(false)
|
||||
.finally(() => httpMock.close());
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
download,
|
||||
hash,
|
||||
checksum,
|
||||
downloadAndCheck,
|
||||
extract,
|
||||
readUTF8,
|
||||
safeDownloadArchived,
|
||||
directoryTree,
|
||||
downloadArchived,
|
||||
search,
|
||||
test
|
||||
};
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
const picker = type => () => new Promise((resolve, reject) => {
|
||||
const fileLoader = e => {
|
||||
const directory = {};
|
||||
const totalFiles = e.target.files.length;
|
||||
let loadedFiles = 0;
|
||||
[].map.call(e.target.files, file => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
const data = new Uint8Array(e.target.result);
|
||||
if (type === "directory") {
|
||||
const path = file.webkitRelativePath;
|
||||
directory[path.slice(path.indexOf("/")+1)] = {
|
||||
type: "text/plain",
|
||||
data: data
|
||||
};
|
||||
if (++loadedFiles === totalFiles)
|
||||
resolve(directory);
|
||||
} else if (type === "file") {
|
||||
const path = file.webkitRelativePath;
|
||||
resolve({"type": mimetype.lookup(path), "data": data});
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
};
|
||||
|
||||
let fileInput;
|
||||
if (type === "directory") {
|
||||
fileInput = document.createElement("input");
|
||||
fileInput.addEventListener("change", fileLoader);
|
||||
fileInput.type = "file";
|
||||
fileInput.webkitdirectory = true;
|
||||
fileInput.mozdirectory = true;
|
||||
fileInput.msdirectory = true;
|
||||
fileInput.odirectory = true;
|
||||
fileInput.directory = true;
|
||||
} else {
|
||||
fileInput = document.createElement("input");
|
||||
fileInput.addEventListener("change", fileLoader);
|
||||
fileInput.type = "file";
|
||||
};
|
||||
|
||||
const mouseEvent = document.createEvent("MouseEvents");
|
||||
mouseEvent.initEvent("click", true, false);
|
||||
fileInput.dispatchEvent(mouseEvent);
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
data: picker("data"),
|
||||
file: picker("file"),
|
||||
directory: picker("directory"),
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
// Thanks https://github.com/axic/swarmhash
|
||||
|
||||
const keccak = require("eth-lib/lib/hash").keccak256;
|
||||
const Bytes = require("eth-lib/lib/bytes");
|
||||
|
||||
const swarmHashBlock = (length, data) => {
|
||||
const lengthEncoded = Bytes.reverse(Bytes.pad(6, Bytes.fromNumber(length)));
|
||||
const bytes = Bytes.flatten([lengthEncoded, "0x0000", data]);
|
||||
return keccak(bytes).slice(2);
|
||||
};
|
||||
|
||||
// (Bytes | Uint8Array | String) -> String
|
||||
const swarmHash = data => {
|
||||
if (typeof data === "string" && data.slice(0,2) !== "0x") {
|
||||
data = Bytes.fromString(data);
|
||||
} else if (typeof data !== "string" && data.length !== undefined) {
|
||||
data = Bytes.fromUint8Array(data);
|
||||
}
|
||||
|
||||
const length = Bytes.length(data);
|
||||
|
||||
if (length <= 4096) {
|
||||
return swarmHashBlock(length, data);
|
||||
}
|
||||
|
||||
let maxSize = 4096;
|
||||
while ((maxSize * (4096 / 32)) < length) {
|
||||
maxSize *= (4096 / 32);
|
||||
}
|
||||
|
||||
let innerNodes = [];
|
||||
for (let i = 0; i < length; i += maxSize) {
|
||||
const size = (maxSize < (length - i)) ? maxSize : (length - i);
|
||||
innerNodes.push(swarmHash(Bytes.slice(data, i, i + size)));
|
||||
}
|
||||
|
||||
return swarmHashBlock(length, Bytes.flatten(innerNodes));
|
||||
};
|
||||
|
||||
module.exports = swarmHash;
|
||||
+505
@@ -0,0 +1,505 @@
|
||||
// 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
|
||||
};
|
||||
|
||||
};
|
||||
Reference in new issue
Block a user