Comparing version 0.1.0 to 2.0.0
# Changelog | ||
## [2.0.0] | ||
_Prior version numbers skipped to maintain parity with API._ | ||
### Added | ||
- `downloadFileHns`, `openFileHns`, `resolveSkylinkHns` | ||
- `getHnsUrl`, `getHnsresUrl` | ||
- `customFilename` and `customDirname` upload options | ||
### Changed | ||
- `download` and `open` were renamed to `downloadFile` and `openFile`. | ||
- `upload` was renamed to `uploadFile` and the response was changed to only | ||
include a skylink. To obtain the full response as in the old `upload`, use the | ||
new `uploadFileRequest`. | ||
- `getDownloadUrl` has been renamed to `getSkylinkUrl`. | ||
- Connection options can now be passed to the client, in addition to individual | ||
API calls, to be applied to all API calls. | ||
- The `defaultPortalUrl` string has been renamed to `defaultSkynetPortalUrl` and | ||
`defaultPortalUrl` is now a function. | ||
## [0.1.0] - 2020-07-29 | ||
@@ -7,3 +29,4 @@ | ||
- New `SkynetClient` class that must be initialized to call methods such as `upload` and `download`. | ||
- New `SkynetClient` class that must be initialized to call methods such as | ||
`upload` and `download`. | ||
- New utility helpers such as `getRelativeFilePath` and `defaultPortalUrl`. | ||
@@ -13,2 +36,4 @@ | ||
- Most standalone functions are now methods on the `SkynetClient`. Previous code that was calling `upload(...)` instead of `client.upload(...)` will no longer work. | ||
- Most standalone functions are now methods on the `SkynetClient`. Previous code | ||
that was calling `upload(...)` instead of `client.upload(...)` will no longer | ||
work. |
@@ -8,11 +8,63 @@ "use strict"; | ||
var _axios = _interopRequireDefault(require("axios")); | ||
var _utils = require("./utils.js"); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
class SkynetClient { | ||
constructor(portalUrl = null) { | ||
if (portalUrl === null) { | ||
portalUrl = (0, _utils.defaultPortalUrl)(); | ||
/** | ||
* The Skynet Client which can be used to access Skynet. | ||
* @constructor | ||
* @param {string} [portalUrl="https://siasky.net"] - The portal URL to use to access Skynet, if specified. To use the default portal while passing custom options, use "". | ||
* @param {Object} [customOptions={}] - Configuration for the client. | ||
* @param {string} [customOptions.method] - HTTP method to use. | ||
* @param {string} [customOptions.APIKey] - Authentication password to use. | ||
* @param {string} [customOptions.customUserAgent=""] - Custom user agent header to set. | ||
* @param {Object} [customOptions.data=null] - Data to send in a POST. | ||
* @param {string} [customOptions.endpointPath=""] - The relative URL path of the portal endpoint to contact. | ||
* @param {string} [customOptions.extraPath=""] - Extra path element to append to the URL. | ||
* @param {Function} [customOptions.onUploadProgress] - Optional callback to track progress. | ||
* @param {Object} [customOptions.query={}] - Query parameters to include in the URl. | ||
*/ | ||
constructor(portalUrl = (0, _utils.defaultPortalUrl)(), customOptions = {}) { | ||
this.portalUrl = portalUrl; | ||
this.customOptions = customOptions; | ||
} | ||
/** | ||
* Creates and executes a request. | ||
* @param {Object} config - Configuration for the request. See docs for constructor for the full list of options. | ||
*/ | ||
executeRequest(config) { | ||
let url = config.url; | ||
if (!url) { | ||
url = (0, _utils.makeUrl)(this.portalUrl, config.endpointPath, config.extraPath ?? ""); | ||
url = (0, _utils.addUrlQuery)(url, config.query); | ||
} | ||
this.portalUrl = portalUrl; | ||
return (0, _axios.default)({ | ||
url: url, | ||
method: config.method, | ||
data: config.data, | ||
headers: config.customUserAgent && { | ||
"User-Agent": config.customUserAgent | ||
}, | ||
auth: config.APIKey && { | ||
username: "", | ||
password: config.APIKey | ||
}, | ||
onUploadProgress: config.onUploadProgress && function ({ | ||
loaded, | ||
total | ||
}) { | ||
const progress = loaded / total; | ||
config.onUploadProgress(progress, { | ||
loaded, | ||
total | ||
}); | ||
} | ||
}); | ||
} | ||
@@ -19,0 +71,0 @@ |
"use strict"; | ||
var _axios = _interopRequireDefault(require("axios")); | ||
var _client = require("./client.js"); | ||
@@ -7,17 +9,50 @@ | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
/* eslint-disable no-unused-vars */ | ||
const defaultDownloadOptions = { ...(0, _utils.defaultOptions)("/") | ||
}; | ||
const defaultDownloadHnsOptions = { ...(0, _utils.defaultOptions)("/hns") | ||
}; | ||
const defaultResolveHnsOptions = { ...(0, _utils.defaultOptions)("/hnsres") | ||
}; | ||
/** | ||
* Initiates a download of the content of the skylink within the browser. | ||
* @param {string} skylink - 46 character skylink. | ||
* @param {Object} [customOptions={}] - Additional settings that can optionally be set. | ||
* @param {string} [customOptions.endpointPath="/"] - The relative URL path of the portal endpoint to contact. | ||
*/ | ||
_client.SkynetClient.prototype.download = function (skylink, customOptions = {}) { | ||
_client.SkynetClient.prototype.downloadFile = function (skylink, customOptions = {}) { | ||
const opts = { ...defaultDownloadOptions, | ||
...this.customOptions, | ||
...customOptions, | ||
download: true | ||
}; | ||
const url = this.getDownloadUrl(skylink, opts); | ||
window.open(url, "_blank"); | ||
const url = this.getSkylinkUrl(skylink, opts); // Download the url. | ||
window.location = url; | ||
}; | ||
/** | ||
* Initiates a download of the content of the skylink at the Handshake domain. | ||
* @param {string} domain - Handshake domain. | ||
* @param {Object} [customOptions={}] - Additional settings that can optionally be set. | ||
* @param {string} [customOptions.endpointPath="/hns"] - The relative URL path of the portal endpoint to contact. | ||
*/ | ||
_client.SkynetClient.prototype.getDownloadUrl = function (skylink, customOptions = {}) { | ||
_client.SkynetClient.prototype.downloadFileHns = async function (domain, customOptions = {}) { | ||
const opts = { ...defaultDownloadHnsOptions, | ||
...this.customOptions, | ||
...customOptions, | ||
download: true | ||
}; | ||
const url = this.getHnsUrl(domain, opts); // Download the url. | ||
window.location = url; | ||
}; | ||
_client.SkynetClient.prototype.getSkylinkUrl = function (skylink, customOptions = {}) { | ||
const opts = { ...defaultDownloadOptions, | ||
...this.customOptions, | ||
...customOptions | ||
@@ -28,7 +63,32 @@ }; | ||
} : {}; | ||
return (0, _utils.makeUrlWithSkylink)(this.portalUrl, opts.endpointPath, skylink, query); | ||
const url = (0, _utils.makeUrl)(this.portalUrl, opts.endpointPath, (0, _utils.parseSkylink)(skylink)); | ||
return (0, _utils.addUrlQuery)(url, query); | ||
}; | ||
_client.SkynetClient.prototype.metadata = function (skylink, customOptions = {}) { | ||
_client.SkynetClient.prototype.getHnsUrl = function (domain, customOptions = {}) { | ||
const opts = { ...defaultDownloadHnsOptions, | ||
...this.customOptions, | ||
...customOptions | ||
}; | ||
const query = opts.download ? { | ||
attachment: true | ||
} : {}; | ||
const url = (0, _utils.makeUrl)(this.portalUrl, opts.endpointPath, (0, _utils.trimUriPrefix)(domain, _utils.uriHandshakePrefix)); | ||
return (0, _utils.addUrlQuery)(url, query); | ||
}; | ||
_client.SkynetClient.prototype.getHnsresUrl = function (domain, customOptions = {}) { | ||
const opts = { ...defaultResolveHnsOptions, | ||
...this.customOptions, | ||
...customOptions | ||
}; | ||
const query = opts.download ? { | ||
attachment: true | ||
} : {}; | ||
return (0, _utils.makeUrl)(this.portalUrl, opts.endpointPath, (0, _utils.trimUriPrefix)(domain, _utils.uriHandshakeResolverPrefix)); | ||
}; | ||
_client.SkynetClient.prototype.getMetadata = async function (skylink, customOptions = {}) { | ||
const opts = { ...defaultDownloadOptions, | ||
...this.customOptions, | ||
...customOptions | ||
@@ -38,9 +98,54 @@ }; | ||
}; | ||
/** | ||
* Opens the content of the skylink within the browser. | ||
* @param {string} skylink - 46 character skylink. | ||
* @param {Object} [customOptions={}] - Additional settings that can optionally be set. | ||
* @param {string} [customOptions.endpointPath="/"] - The relative URL path of the portal endpoint to contact. | ||
*/ | ||
_client.SkynetClient.prototype.open = function (skylink, customOptions = {}) { | ||
_client.SkynetClient.prototype.openFile = function (skylink, customOptions = {}) { | ||
const opts = { ...defaultDownloadOptions, | ||
...this.customOptions, | ||
...customOptions | ||
}; | ||
const url = (0, _utils.makeUrlWithSkylink)(this.portalUrl, opts.endpointPath, skylink); | ||
const url = this.getSkylinkUrl(skylink, opts); | ||
window.open(url, "_blank"); | ||
}; | ||
/** | ||
* Opens the content of the skylink from the given Handshake domain within the browser. | ||
* @param {string} domain - Handshake domain. | ||
* @param {Object} [customOptions={}] - Additional settings that can optionally be set. | ||
* @param {string} [customOptions.endpointPath="/hns"] - The relative URL path of the portal endpoint to contact. | ||
*/ | ||
_client.SkynetClient.prototype.openFileHns = async function (domain, customOptions = {}) { | ||
const opts = { ...defaultDownloadHnsOptions, | ||
...this.customOptions, | ||
...customOptions | ||
}; | ||
const url = this.getHnsUrl(domain, opts); // Open the url in a new tab. | ||
window.open(url, "_blank"); | ||
}; | ||
/** | ||
* @param {string} domain - Handshake resolver domain. | ||
* @param {Object} [customOptions={}] - Additional settings that can optionally be set. | ||
* @param {string} [customOptions.endpointPath="/hnsres"] - The relative URL path of the portal endpoint to contact. | ||
*/ | ||
_client.SkynetClient.prototype.resolveHns = async function (domain, customOptions = {}) { | ||
const opts = { ...defaultResolveHnsOptions, | ||
...this.customOptions, | ||
...customOptions | ||
}; | ||
const url = this.getHnsresUrl(domain, opts); // Get the txt record from the hnsres domain on the portal. | ||
const response = await this.executeRequest({ ...opts, | ||
method: "get", | ||
url | ||
}); | ||
return response.data; | ||
}; |
"use strict"; | ||
var _axios = _interopRequireDefault(require("axios")); | ||
var _axiosMockAdapter = _interopRequireDefault(require("axios-mock-adapter")); | ||
var _index = require("./index"); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
const mock = new _axiosMockAdapter.default(_axios.default); | ||
const portalUrl = _index.defaultSkynetPortalUrl; | ||
const hnsLink = "foo"; | ||
const hnsUrl = `${portalUrl}/hns/${hnsLink}`; | ||
const hnsresUrl = `${portalUrl}/hnsres/${hnsLink}`; | ||
const client = new _index.SkynetClient(portalUrl); | ||
const skylink = "XABvi7JtJbQSMAcDwnUnmp2FKDPjg8_tTTFP4BwMSxVdEg"; | ||
const validSkylinkVariations = [skylink, `sia:${skylink}`, `sia://${skylink}`, `${portalUrl}/${skylink}`, `${portalUrl}/${skylink}/foo/bar`, `${portalUrl}/${skylink}?foo=bar`]; | ||
describe("download", () => { | ||
const validHnsLinkVariations = [hnsLink, `hns:${hnsLink}`, `hns://${hnsLink}`]; | ||
const validHnsresLinkVariations = [hnsLink, `hnsres:${hnsLink}`, `hnsres://${hnsLink}`]; | ||
describe("downloadFile", () => { | ||
it("should call window.open with a download url with attachment set", () => { | ||
@@ -14,27 +26,47 @@ const windowOpen = jest.spyOn(window, "open").mockImplementation(); | ||
windowOpen.mockReset(); | ||
client.download(input); | ||
expect(windowOpen).toHaveBeenCalledTimes(1); | ||
expect(windowOpen).toHaveBeenCalledWith(`${portalUrl}/${skylink}?attachment=true`, "_blank"); | ||
client.downloadFile(input); | ||
expect(mock.history.get.length).toBe(0); | ||
}); | ||
}); | ||
}); | ||
describe("getDownloadUrl", () => { | ||
it("should return correctly formed download URL", () => { | ||
validSkylinkVariations.forEach(input => { | ||
expect(client.getDownloadUrl(input)).toEqual(`${portalUrl}/${skylink}`); | ||
describe("getHnsUrl", () => { | ||
it("should return correctly formed hns URL", () => { | ||
validHnsLinkVariations.forEach(input => { | ||
expect(client.getHnsUrl(input)).toEqual(hnsUrl); | ||
}); | ||
}); | ||
it("should return correctly formed url with forced download", () => { | ||
const url = client.getDownloadUrl(skylink, { | ||
it("should return correctly formed hns URL with forced download", () => { | ||
const url = client.getHnsUrl(hnsLink, { | ||
download: true | ||
}); | ||
expect(url).toEqual(`${portalUrl}/${skylink}?attachment=true`); | ||
expect(url).toEqual(`${hnsUrl}?attachment=true`); | ||
}); | ||
}); | ||
describe("getHnsresUrl", () => { | ||
it("should return correctly formed hnsres URL", () => { | ||
validHnsresLinkVariations.forEach(input => { | ||
expect(client.getHnsresUrl(input)).toEqual(hnsresUrl); | ||
}); | ||
}); | ||
}); | ||
describe("getSkylinkUrl", () => { | ||
it("should return correctly formed skylink URL", () => { | ||
validSkylinkVariations.forEach(input => { | ||
expect(client.getSkylinkUrl(input)).toEqual(`${portalUrl}/${skylink}`); | ||
}); | ||
}); | ||
it("should return correctly formed URL with forced download", () => { | ||
const url = client.getSkylinkUrl(skylink, { | ||
download: true, | ||
endpointPath: "skynet/skylink" | ||
}); | ||
expect(url).toEqual(`${portalUrl}/skynet/skylink/${skylink}?attachment=true`); | ||
}); | ||
}); | ||
describe("open", () => { | ||
it("should call window.open with a download url", () => { | ||
it("should call window.openFile", () => { | ||
const windowOpen = jest.spyOn(window, "open").mockImplementation(); | ||
validSkylinkVariations.forEach(input => { | ||
windowOpen.mockReset(); | ||
client.open(input); | ||
client.openFile(input); | ||
expect(windowOpen).toHaveBeenCalledTimes(1); | ||
@@ -44,2 +76,44 @@ expect(windowOpen).toHaveBeenCalledWith(`${portalUrl}/${skylink}`, "_blank"); | ||
}); | ||
}); | ||
describe("downloadFileHns", () => { | ||
it("should set domain with the portal and hns link and then call window.openFile with attachment set", async () => { | ||
const windowOpen = jest.spyOn(window, "open").mockImplementation(); | ||
for (const input of validHnsLinkVariations) { | ||
mock.resetHistory(); | ||
windowOpen.mockReset(); | ||
await client.downloadFileHns(input); | ||
expect(mock.history.get.length).toBe(0); | ||
} | ||
}); | ||
}); | ||
describe("openFileHns", () => { | ||
const hnsUrl = `${portalUrl}/hns/${hnsLink}`; | ||
it("should set domain with the portal and hns link and then call window.openFile", async () => { | ||
const windowOpen = jest.spyOn(window, "open").mockImplementation(); | ||
for (const input of validHnsLinkVariations) { | ||
mock.resetHistory(); | ||
windowOpen.mockReset(); | ||
await client.openFileHns(input); | ||
expect(mock.history.get.length).toBe(0); | ||
expect(windowOpen).toHaveBeenCalledTimes(1); | ||
expect(windowOpen).toHaveBeenCalledWith(hnsUrl, "_blank"); | ||
} | ||
}); | ||
}); | ||
describe("resolveHns", () => { | ||
beforeEach(() => { | ||
mock.onGet(hnsresUrl).reply(200, { | ||
skylink: skylink | ||
}); | ||
}); | ||
it("should call axios.get with the portal and hnsres link and return the json body", async () => { | ||
for (const input of validHnsresLinkVariations) { | ||
mock.resetHistory(); | ||
const data = await client.resolveHns(input); | ||
expect(mock.history.get.length).toBe(1); | ||
expect(data.skylink).toEqual(skylink); | ||
} | ||
}); | ||
}); |
"use strict"; | ||
var _client = require("./client.js"); | ||
var _utils = require("./utils.js"); | ||
@@ -15,20 +17,20 @@ | ||
_utils.SkynetClient.prototype.addSkykey = async function (skykey, customOptions = {}) { | ||
_client.SkynetClient.prototype.addSkykey = async function (skykey, customOptions = {}) { | ||
throw new Error("Unimplemented"); | ||
}; | ||
_utils.SkynetClient.prototype.createSkykey = async function (skykeyName, skykeyType, customOptions = {}) { | ||
_client.SkynetClient.prototype.createSkykey = async function (skykeyName, skykeyType, customOptions = {}) { | ||
throw new Error("Unimplemented"); | ||
}; | ||
_utils.SkynetClient.prototype.getSkykeyById = async function (skykeyId, customOptions = {}) { | ||
_client.SkynetClient.prototype.getSkykeyById = async function (skykeyId, customOptions = {}) { | ||
throw new Error("Unimplemented"); | ||
}; | ||
_utils.SkynetClient.prototype.getSkykeyByName = async function (skykeyName, customOptions = {}) { | ||
_client.SkynetClient.prototype.getSkykeyByName = async function (skykeyName, customOptions = {}) { | ||
throw new Error("Unimplemented"); | ||
}; | ||
_utils.SkynetClient.prototype.getSkykeys = async function getSkykeys(customOptions = {}) { | ||
_client.SkynetClient.prototype.getSkykeys = async function getSkykeys(customOptions = {}) { | ||
throw new Error("Unimplemented"); | ||
}; |
@@ -42,2 +42,20 @@ "use strict"; | ||
}); | ||
Object.defineProperty(exports, "uriHandshakePrefix", { | ||
enumerable: true, | ||
get: function () { | ||
return _utils.uriHandshakePrefix; | ||
} | ||
}); | ||
Object.defineProperty(exports, "uriHandshakeResolverPrefix", { | ||
enumerable: true, | ||
get: function () { | ||
return _utils.uriHandshakeResolverPrefix; | ||
} | ||
}); | ||
Object.defineProperty(exports, "uriSkynetPrefix", { | ||
enumerable: true, | ||
get: function () { | ||
return _utils.uriSkynetPrefix; | ||
} | ||
}); | ||
@@ -48,4 +66,6 @@ var _client = require("./client.js"); | ||
require("./encryption.js"); | ||
require("./upload.js"); | ||
var _utils = require("./utils.js"); |
@@ -9,9 +9,23 @@ "use strict"; | ||
expect(client).toHaveProperty("download"); | ||
expect(client).toHaveProperty("getDownloadUrl"); | ||
expect(client).toHaveProperty("open"); // Upload | ||
expect(client).toHaveProperty("downloadFile"); | ||
expect(client).toHaveProperty("downloadFileHns"); | ||
expect(client).toHaveProperty("getHnsUrl"); | ||
expect(client).toHaveProperty("getHnsresUrl"); | ||
expect(client).toHaveProperty("getSkylinkUrl"); | ||
expect(client).toHaveProperty("getMetadata"); | ||
expect(client).toHaveProperty("openFile"); | ||
expect(client).toHaveProperty("openFileHns"); | ||
expect(client).toHaveProperty("resolveHns"); // Encryption | ||
expect(client).toHaveProperty("upload"); | ||
expect(client).toHaveProperty("addSkykey"); | ||
expect(client).toHaveProperty("createSkykey"); | ||
expect(client).toHaveProperty("getSkykeyById"); | ||
expect(client).toHaveProperty("getSkykeyByName"); | ||
expect(client).toHaveProperty("getSkykeys"); // Upload | ||
expect(client).toHaveProperty("uploadFile"); | ||
expect(client).toHaveProperty("uploadFileRequest"); | ||
expect(client).toHaveProperty("uploadDirectory"); | ||
expect(client).toHaveProperty("uploadDirectoryRequest"); | ||
}); | ||
}); |
"use strict"; | ||
var _axios = _interopRequireDefault(require("axios")); | ||
var _client = require("./client.js"); | ||
@@ -9,31 +7,27 @@ | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
const defaultUploadOptions = { ...(0, _utils.defaultOptions)("/skynet/skyfile"), | ||
portalFileFieldname: "file", | ||
portalDirectoryFileFieldname: "files[]" // TODO: | ||
// customFilename: "", | ||
portalDirectoryFileFieldname: "files[]", | ||
customFilename: "" | ||
}; | ||
_client.SkynetClient.prototype.uploadFile = async function (file, customOptions = {}) { | ||
const response = await this.uploadFileRequest(file, customOptions); | ||
return `${_utils.uriSkynetPrefix}${response.skylink}`; | ||
}; | ||
_client.SkynetClient.prototype.upload = async function (file, customOptions = {}) { | ||
_client.SkynetClient.prototype.uploadFileRequest = async function (file, customOptions = {}) { | ||
const opts = { ...defaultUploadOptions, | ||
...this.customOptions, | ||
...customOptions | ||
}; | ||
const formData = new FormData(); | ||
formData.append(opts.portalFileFieldname, ensureFileObjectConsistency(file)); | ||
const url = (0, _utils.makeUrl)(this.portalUrl, opts.endpointPath); | ||
file = ensureFileObjectConsistency(file); | ||
const filename = opts.customFilename ? opts.customFilename : ""; | ||
formData.append(opts.portalFileFieldname, file, filename); | ||
const { | ||
data | ||
} = await _axios.default.post(url, formData, opts.onUploadProgress && { | ||
onUploadProgress: ({ | ||
loaded, | ||
total | ||
}) => { | ||
const progress = loaded / total; | ||
opts.onUploadProgress(progress, { | ||
loaded, | ||
total | ||
}); | ||
} | ||
} = await this.executeRequest({ ...opts, | ||
method: "post", | ||
data: formData | ||
}); | ||
@@ -47,4 +41,11 @@ return data; | ||
* @param {Object} [customOptions={}] - Additional settings that can optionally be set. | ||
* @param {string} [config.APIKey] - Authentication password to use. | ||
* @param {string} [config.customUserAgent=""] - Custom user agent header to set. | ||
* @param {string} [customOptions.endpointPath="/skynet/skyfile"] - The relative URL path of the portal endpoint to contact. | ||
* @param {Function} [config.onUploadProgress] - Optional callback to track progress. | ||
* @param {string} [customOptions.portalDirectoryfilefieldname="files[]"] - The fieldName for directory files on the portal. | ||
* @returns {Object} data - The returned data. | ||
* @returns {string} data.skylink - The returned skylink. | ||
* @returns {string} data.merkleroot - The hash that is encoded into the skylink. | ||
* @returns {number} data.bitfield - The bitfield that gets encoded into the skylink. | ||
*/ | ||
@@ -54,3 +55,9 @@ | ||
_client.SkynetClient.prototype.uploadDirectory = async function (directory, filename, customOptions = {}) { | ||
const response = await this.uploadDirectoryRequest(directory, filename, customOptions); | ||
return `${_utils.uriSkynetPrefix}${response.skylink}`; | ||
}; | ||
_client.SkynetClient.prototype.uploadDirectoryRequest = async function (directory, filename, customOptions = {}) { | ||
const opts = { ...defaultUploadOptions, | ||
...this.customOptions, | ||
...customOptions | ||
@@ -60,19 +67,12 @@ }; | ||
Object.entries(directory).forEach(([path, file]) => { | ||
formData.append(opts.portalDirectoryFileFieldname, ensureFileObjectConsistency(file), path); | ||
file = ensureFileObjectConsistency(file); | ||
formData.append(opts.portalDirectoryFileFieldname, file, path); | ||
}); | ||
const url = (0, _utils.makeUrl)(this.portalUrl, opts.endpointPath, { | ||
filename | ||
}); | ||
const { | ||
data | ||
} = await _axios.default.post(url, formData, opts.onUploadProgress && { | ||
onUploadProgress: ({ | ||
loaded, | ||
total | ||
}) => { | ||
const progress = loaded / total; | ||
opts.onUploadProgress(progress, { | ||
loaded, | ||
total | ||
}); | ||
} = await this.executeRequest({ ...opts, | ||
method: "post", | ||
data: formData, | ||
query: { | ||
filename | ||
} | ||
@@ -79,0 +79,0 @@ }); |
@@ -5,11 +5,17 @@ "use strict"; | ||
var _axiosMockAdapter = _interopRequireDefault(require("axios-mock-adapter")); | ||
var _index = require("./index"); | ||
var _test_utils = require("./test_utils.js"); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
jest.mock("axios"); | ||
const mock = new _axiosMockAdapter.default(_axios.default); | ||
const portalUrl = _index.defaultSkynetPortalUrl; | ||
const client = new _index.SkynetClient(portalUrl); | ||
const skylink = "XABvi7JtJbQSMAcDwnUnmp2FKDPjg8_tTTFP4BwMSxVdEg"; | ||
const sialink = `${_index.uriSkynetPrefix}${skylink}`; | ||
describe("uploadFile", () => { | ||
const url = `${portalUrl}/skynet/skyfile`; | ||
const filename = "bar.txt"; | ||
@@ -20,74 +26,112 @@ const file = new File(["foo"], filename, { | ||
beforeEach(() => { | ||
_axios.default.post.mockResolvedValue({ | ||
data: { | ||
skylink | ||
} | ||
mock.onPost(url).reply(200, { | ||
skylink: skylink | ||
}); | ||
mock.resetHistory(); | ||
}); | ||
it("should send post request with FormData", () => { | ||
client.upload(file, {}); | ||
expect(_axios.default.post).toHaveBeenCalledWith(`${portalUrl}/skynet/skyfile`, expect.any(FormData), // TODO: Inspect data contents. | ||
undefined); | ||
it("should send formdata with file", async () => { | ||
const data = await client.uploadFile(file); | ||
expect(mock.history.post.length).toBe(1); | ||
const request = mock.history.post[0]; | ||
await (0, _test_utils.compareFormData)(request.data, [["file", "foo", filename]]); | ||
expect(data).toEqual(sialink); | ||
}); | ||
it("should send register onUploadProgress callback if defined", () => { | ||
it("should send register onUploadProgress callback if defined", async () => { | ||
const newPortal = "https://my-portal.net"; | ||
const client = new _index.SkynetClient(newPortal); | ||
client.upload(file, { | ||
const url = `${newPortal}/skynet/skyfile`; | ||
const client = new _index.SkynetClient(newPortal); // Use replyOnce to catch a single request with the new URL. | ||
mock.onPost(url).replyOnce(200, { | ||
skylink: skylink | ||
}); | ||
const data = await client.uploadFile(file, { | ||
onUploadProgress: jest.fn() | ||
}); | ||
expect(_axios.default.post).toHaveBeenCalledWith(`${newPortal}/skynet/skyfile`, expect.any(FormData), // TODO: Inspect data contents. | ||
{ | ||
onUploadProgress: expect.any(Function) | ||
expect(mock.history.post.length).toBe(1); | ||
const request = mock.history.post[0]; | ||
expect(request.onUploadProgress).toEqual(expect.any(Function)); | ||
await (0, _test_utils.compareFormData)(request.data, [["file", "foo", filename]]); | ||
expect(data).toEqual(sialink); | ||
}); | ||
it("should use custom filename if provided", async () => { | ||
const data = await client.uploadFile(file, { | ||
customFilename: "testname" | ||
}); | ||
expect(mock.history.post.length).toBe(1); | ||
const request = mock.history.post[0]; | ||
await (0, _test_utils.compareFormData)(request.data, [["file", "foo", "testname"]]); | ||
expect(data).toEqual(sialink); | ||
}); | ||
it("should send base-64 authentication password if provided", () => { | ||
client.upload(file, { | ||
it("should send base-64 authentication password if provided", async () => { | ||
const data = await client.uploadFile(file, { | ||
APIKey: "foobar" | ||
}); | ||
expect(_axios.default.post).toHaveBeenCalledWith(`${portalUrl}/skynet/skyfile`, expect.any(FormData), // TODO: Inspect data contents. | ||
undefined); | ||
expect(mock.history.post.length).toBe(1); | ||
const request = mock.history.post[0]; | ||
expect(request.auth).toEqual({ | ||
username: "", | ||
password: "foobar" | ||
}); | ||
await (0, _test_utils.compareFormData)(request.data, [["file", "foo", filename]]); | ||
expect(data).toEqual(sialink); | ||
}); | ||
it("should return skylink on success", async () => { | ||
const data = await client.upload(file); | ||
expect(data).toEqual({ | ||
skylink | ||
it("should send custom user agent if defined", async () => { | ||
const client = new _index.SkynetClient(portalUrl, { | ||
customUserAgent: "Sia-Agent" | ||
}); | ||
const data = await client.uploadFile(file); | ||
expect(mock.history.post.length).toBe(1); | ||
const request = mock.history.post[0]; | ||
expect(request.headers["User-Agent"]).toEqual("Sia-Agent"); // Check that other headers weren't altered. | ||
expect(request.headers["Content-Type"]).toEqual("application/x-www-form-urlencoded"); | ||
await (0, _test_utils.compareFormData)(request.data, [["file", "foo", filename]]); | ||
expect(data).toEqual(sialink); | ||
}); | ||
it("Should use user agent set in options to function", async () => { | ||
const client = new _index.SkynetClient(portalUrl, { | ||
customUserAgent: "Sia-Agent" | ||
}); | ||
const data = await client.uploadFile(file, { | ||
customUserAgent: "Sia-Agent-2" | ||
}); | ||
expect(mock.history.post.length).toBe(1); | ||
const request = mock.history.post[0]; | ||
expect(request.headers["User-Agent"]).toEqual("Sia-Agent-2"); // Check that other headers weren't altered. | ||
expect(request.headers["Content-Type"]).toEqual("application/x-www-form-urlencoded"); | ||
await (0, _test_utils.compareFormData)(request.data, [["file", "foo", filename]]); | ||
expect(data).toEqual(sialink); | ||
}); | ||
}); | ||
describe("uploadDirectory", () => { | ||
const blob = new Blob([], { | ||
type: "image/jpeg" | ||
}); | ||
const filename = "i-am-root"; | ||
const directory = { | ||
"i-am-not/file1.jpeg": new File([blob], "i-am-not/file1.jpeg"), | ||
"i-am-not/file2.jpeg": new File([blob], "i-am-not/file2.jpeg"), | ||
"i-am-not/me-neither/file3.jpeg": new File([blob], "i-am-not/me-neither/file3.jpeg") | ||
"i-am-not/file1.jpeg": new File(["foo1"], "i-am-not/file1.jpeg"), | ||
"i-am-not/file2.jpeg": new File(["foo2"], "i-am-not/file2.jpeg"), | ||
"i-am-not/me-neither/file3.jpeg": new File(["foo3"], "i-am-not/me-neither/file3.jpeg") | ||
}; | ||
const url = `${portalUrl}/skynet/skyfile?filename=${filename}`; | ||
beforeEach(() => { | ||
_axios.default.post.mockResolvedValue({ | ||
data: { | ||
skylink | ||
} | ||
mock.onPost(url).reply(200, { | ||
skylink: skylink | ||
}); | ||
mock.resetHistory(); | ||
}); | ||
it("should send post request with FormData", () => { | ||
client.uploadDirectory(directory, filename); | ||
expect(_axios.default.post).toHaveBeenCalledWith(`${portalUrl}/skynet/skyfile?filename=${filename}`, expect.any(FormData), // TODO: Inspect data contents. | ||
undefined); | ||
it("should send formdata with files", async () => { | ||
const data = await client.uploadDirectory(directory, filename); | ||
expect(mock.history.post.length).toBe(1); | ||
const request = mock.history.post[0]; | ||
await (0, _test_utils.compareFormData)(request.data, [["files[]", "foo1", "i-am-not/file1.jpeg"], ["files[]", "foo2", "i-am-not/file2.jpeg"], ["files[]", "foo3", "i-am-not/me-neither/file3.jpeg"]]); | ||
expect(data).toEqual(sialink); | ||
}); | ||
it("should send register onUploadProgress callback if defined", () => { | ||
client.uploadDirectory(directory, filename, { | ||
it("should send register onUploadProgress callback if defined", async () => { | ||
const data = await client.uploadDirectory(directory, filename, { | ||
onUploadProgress: jest.fn() | ||
}); | ||
expect(_axios.default.post).toHaveBeenCalledWith(`${portalUrl}/skynet/skyfile?filename=${filename}`, expect.any(FormData), { | ||
onUploadProgress: expect.any(Function) | ||
}); | ||
expect(mock.history.post.length).toBe(1); | ||
const request = mock.history.post[0]; | ||
expect(request.onUploadProgress).toEqual(expect.any(Function)); | ||
expect(data).toEqual(sialink); | ||
}); | ||
it("should return single skylink on success", async () => { | ||
const data = await client.uploadDirectory(directory, filename); | ||
expect(data).toEqual({ | ||
skylink | ||
}); | ||
}); | ||
}); |
@@ -6,10 +6,11 @@ "use strict"; | ||
}); | ||
exports.addUrlQuery = addUrlQuery; | ||
exports.defaultOptions = defaultOptions; | ||
exports.defaultPortalUrl = defaultPortalUrl; | ||
exports.defaultOptions = defaultOptions; | ||
exports.getRelativeFilePath = getRelativeFilePath; | ||
exports.getRootDirectory = getRootDirectory; | ||
exports.makeUrl = makeUrl; | ||
exports.makeUrlWithSkylink = makeUrlWithSkylink; | ||
exports.parseSkylink = parseSkylink; | ||
exports.defaultSkynetPortalUrl = void 0; | ||
exports.trimUriPrefix = trimUriPrefix; | ||
exports.uriSkynetPrefix = exports.uriHandshakeResolverPrefix = exports.uriHandshakePrefix = exports.defaultSkynetPortalUrl = void 0; | ||
@@ -20,2 +21,4 @@ var _pathBrowserify = _interopRequireDefault(require("path-browserify")); | ||
var _urlJoin = _interopRequireDefault(require("url-join")); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
@@ -26,6 +29,13 @@ | ||
exports.defaultSkynetPortalUrl = defaultSkynetPortalUrl; | ||
const uriHandshakePrefix = "hns:"; | ||
exports.uriHandshakePrefix = uriHandshakePrefix; | ||
const uriHandshakeResolverPrefix = "hnsres:"; | ||
exports.uriHandshakeResolverPrefix = uriHandshakeResolverPrefix; | ||
const uriSkynetPrefix = "sia:"; | ||
exports.uriSkynetPrefix = uriSkynetPrefix; | ||
function defaultPortalUrl() { | ||
var url = new URL(window.location.href); | ||
return url.href.substring(0, url.href.indexOf(url.pathname)); | ||
function addUrlQuery(url, query) { | ||
const parsed = (0, _urlParse.default)(url); | ||
parsed.set("query", query); | ||
return parsed.toString(); | ||
} | ||
@@ -35,25 +45,15 @@ | ||
return { | ||
endpointPath: endpointPath // TODO: | ||
// APIKey: "", | ||
// customUserAgent: "", | ||
endpointPath: endpointPath, | ||
APIKey: "", | ||
customUserAgent: "" | ||
}; | ||
} // TODO: Use this to simplify creating requests. Needs to be tested. | ||
// export function executeRequest(portalUrl, method, opts, query, data = {}) { | ||
// const url = makeUrl(portalUrl, opts.endpointPath, query); | ||
// return axios({ | ||
// method: method, | ||
// url: url, | ||
// data: data, | ||
// auth: opts.APIKey && {username: "", password: opts.APIKey }, | ||
// onUploadProgress: opts.onUploadProgress && { | ||
// onUploadProgress: ({ loaded, total }) => { | ||
// const progress = loaded / total; | ||
// opts.onUploadProgress(progress, { loaded, total }); | ||
// }, | ||
// } | ||
// }); | ||
// } | ||
} // TODO: This will be smarter. See | ||
// https://github.com/NebulousLabs/skynet-docs/issues/21. | ||
function defaultPortalUrl() { | ||
var url = new URL(window.location.href); | ||
return url.href.substring(0, url.href.indexOf(url.pathname)); | ||
} | ||
function getFilePath(file) { | ||
@@ -87,13 +87,13 @@ return file.webkitRelativePath || file.path || file.name; | ||
} | ||
/** | ||
* Properly joins paths together to create a URL. Takes a variable number of | ||
* arguments. | ||
* @returns {string} url - The URL. | ||
*/ | ||
function makeUrl(portalUrl, pathname, query = {}) { | ||
const parsed = (0, _urlParse.default)(portalUrl); | ||
parsed.set("pathname", pathname); | ||
parsed.set("query", query); | ||
return parsed.toString(); | ||
} | ||
function makeUrlWithSkylink(portalUrl, endpointPath, skylink, query = {}) { | ||
const parsedSkylink = parseSkylink(skylink); | ||
return makeUrl(portalUrl, _pathBrowserify.default.posix.join(endpointPath, parsedSkylink), query); | ||
function makeUrl(...args) { | ||
return args.reduce(function (acc, cur) { | ||
return (0, _urlJoin.default)(acc, cur); | ||
}); | ||
} | ||
@@ -103,7 +103,6 @@ | ||
const SKYLINK_DIRECT_REGEX = new RegExp(`^${SKYLINK_MATCHER}$`); | ||
const SKYLINK_SIA_PREFIXED_REGEX = new RegExp(`^sia:(?://)?${SKYLINK_MATCHER}$`); | ||
const SKYLINK_PATHNAME_REGEX = new RegExp(`^/${SKYLINK_MATCHER}`); | ||
const SKYLINK_REGEXP_MATCH_POSITION = 1; | ||
function parseSkylink(skylink = "") { | ||
function parseSkylink(skylink) { | ||
if (typeof skylink !== "string") throw new Error(`Skylink has to be a string, ${typeof skylink} provided`); // check for direct skylink match | ||
@@ -116,4 +115,3 @@ | ||
const matchSiaPrefixed = skylink.match(SKYLINK_SIA_PREFIXED_REGEX); | ||
if (matchSiaPrefixed) return matchSiaPrefixed[SKYLINK_REGEXP_MATCH_POSITION]; // check for skylink passed in an url and extract it | ||
skylink = trimUriPrefix(skylink, uriSkynetPrefix); // check for skylink passed in an url and extract it | ||
// example: https://siasky.net/XABvi7JtJbQSMAcDwnUnmp2FKDPjg8_tTTFP4BwMSxVdEg | ||
@@ -125,2 +123,18 @@ | ||
throw new Error(`Could not extract skylink from '${skylink}'`); | ||
} | ||
function trimUriPrefix(str, prefix) { | ||
const longPrefix = `${prefix}//`; | ||
if (str.startsWith(longPrefix)) { | ||
// longPrefix is exactly at the beginning | ||
return str.slice(longPrefix.length); | ||
} | ||
if (str.startsWith(prefix)) { | ||
// else prefix is exactly at the beginning | ||
return str.slice(prefix.length); | ||
} | ||
return str; | ||
} |
@@ -7,22 +7,46 @@ "use strict"; | ||
const skylink = "XABvi7JtJbQSMAcDwnUnmp2FKDPjg8_tTTFP4BwMSxVdEg"; | ||
const validSkylinkVariations = [skylink, `sia:${skylink}`, `sia://${skylink}`, `${portalUrl}/${skylink}`, `${portalUrl}/${skylink}/foo/bar`, `${portalUrl}/${skylink}?foo=bar`]; | ||
const hnsLink = "doesn"; | ||
const hnsresLink = "doesn"; | ||
describe("addUrlQuery", () => { | ||
it("should return correctly formed URLs with query parameters", () => { | ||
expect((0, _utils.addUrlQuery)(portalUrl, { | ||
filename: "test" | ||
})).toEqual(`${portalUrl}?filename=test`); | ||
expect((0, _utils.addUrlQuery)(`${portalUrl}/path/`, { | ||
download: true | ||
})).toEqual(`${portalUrl}/path/?download=true`); | ||
expect((0, _utils.addUrlQuery)(`${portalUrl}/skynet/`, { | ||
foo: 1, | ||
bar: 2 | ||
})).toEqual(`${portalUrl}/skynet/?foo=1&bar=2`); | ||
expect((0, _utils.addUrlQuery)(`${portalUrl}/`, { | ||
attachment: true | ||
})).toEqual(`${portalUrl}/?attachment=true`); | ||
}); | ||
}); | ||
describe("makeUrl", () => { | ||
it("should return correctly formed URLs", () => { | ||
expect((0, _utils.makeUrl)(portalUrl, "/")).toEqual(`${portalUrl}/`); | ||
expect((0, _utils.makeUrl)(portalUrl, "/", { | ||
attachment: true | ||
})).toEqual(`${portalUrl}/?attachment=true`); | ||
expect((0, _utils.makeUrl)(portalUrl, "/skynet")).toEqual(`${portalUrl}/skynet`); | ||
expect((0, _utils.makeUrl)(portalUrl, "/skynet/")).toEqual(`${portalUrl}/skynet/`); | ||
expect((0, _utils.makeUrl)(portalUrl, "/skynet/", { | ||
foo: 1, | ||
bar: 2 | ||
})).toEqual(`${portalUrl}/skynet/?foo=1&bar=2`); | ||
expect((0, _utils.makeUrlWithSkylink)(portalUrl, "/", skylink)).toEqual(`${portalUrl}/${skylink}`); | ||
expect((0, _utils.makeUrlWithSkylink)(portalUrl, "/skynet", skylink)).toEqual(`${portalUrl}/skynet/${skylink}`); | ||
expect((0, _utils.makeUrlWithSkylink)(portalUrl, "/skynet/", skylink)).toEqual(`${portalUrl}/skynet/${skylink}`); | ||
expect((0, _utils.makeUrl)(portalUrl, "/", skylink)).toEqual(`${portalUrl}/${skylink}`); | ||
expect((0, _utils.makeUrl)(portalUrl, "/skynet", skylink)).toEqual(`${portalUrl}/skynet/${skylink}`); | ||
expect((0, _utils.makeUrl)(portalUrl, "//skynet/", skylink)).toEqual(`${portalUrl}/skynet/${skylink}`); | ||
}); | ||
}); | ||
describe("trimUriPrefix", () => { | ||
it("should correctly parse hns prefixed link", () => { | ||
const validHnsLinkVariations = [hnsLink, `hns:${hnsLink}`, `hns://${hnsLink}`]; | ||
const validHnsresLinkVariations = [hnsresLink, `hnsres:${hnsresLink}`, `hnsres://${hnsresLink}`]; | ||
validHnsLinkVariations.forEach(input => { | ||
expect((0, _utils.trimUriPrefix)(input, _utils.uriHandshakePrefix)).toEqual(hnsLink); | ||
}); | ||
validHnsresLinkVariations.forEach(input => { | ||
expect((0, _utils.trimUriPrefix)(input, _utils.uriHandshakeResolverPrefix)).toEqual(hnsresLink); | ||
}); | ||
}); | ||
}); | ||
describe("parseSkylink", () => { | ||
it("should correctly parse skylink out of different strings", () => { | ||
const validSkylinkVariations = [skylink, `sia:${skylink}`, `sia://${skylink}`, `${portalUrl}/${skylink}`, `${portalUrl}/${skylink}/foo/bar`, `${portalUrl}/${skylink}?foo=bar`]; | ||
validSkylinkVariations.forEach(input => { | ||
@@ -33,3 +57,3 @@ expect((0, _utils.parseSkylink)(input)).toEqual(skylink); | ||
it("should throw on invalid skylink", () => { | ||
expect(() => (0, _utils.parseSkylink)()).toThrowError("Could not extract skylink from ''"); | ||
expect(() => (0, _utils.parseSkylink)()).toThrowError("Skylink has to be a string, undefined provided"); | ||
expect(() => (0, _utils.parseSkylink)(123)).toThrowError("Skylink has to be a string, number provided"); | ||
@@ -36,0 +60,0 @@ expect(() => (0, _utils.parseSkylink)("123")).toThrowError("Could not extract skylink from '123'"); |
{ | ||
"name": "skynet-js", | ||
"version": "0.1.0", | ||
"version": "2.0.0", | ||
"description": "Sia Skynet Javascript Client", | ||
@@ -12,3 +12,3 @@ "main": "dist/index.js", | ||
"scripts": { | ||
"test": "jest", | ||
"test": "jest --runInBand", | ||
"prepublishOnly": "babel src --out-dir dist" | ||
@@ -44,4 +44,5 @@ }, | ||
"dependencies": { | ||
"axios": "^0.19.2", | ||
"axios": "^0.20.0", | ||
"path-browserify": "^1.0.1", | ||
"url-join": "^4.0.1", | ||
"url-parse": "^1.4.7" | ||
@@ -53,2 +54,3 @@ }, | ||
"@babel/preset-env": "^7.9.0", | ||
"axios-mock-adapter": "^1.18.2", | ||
"babel-jest": "^26.0.1", | ||
@@ -55,0 +57,0 @@ "eslint": "^7.1.0", |
166
README.md
@@ -25,166 +25,8 @@ # skynet-js - Javascript Sia Skynet Client | ||
## Docs | ||
## Documentation | ||
### Using SkynetClient | ||
For documentation complete with examples, please see [the Skynet SDK docs](https://nebulouslabs.github.io/skynet-docs/?javascript--browser#introduction). | ||
Client implements all the standalone functions as methods with bound `portalUrl` so you don't need to repeat it every time. | ||
### Browser Utility Functions | ||
`portalUrl` (string) - Optional portal url. If not specified, will try to use the current portal that the sky app is running inside of. | ||
```javascript | ||
import { SkynetClient } from "skynet-js"; | ||
const client = new SkynetClient("https://siasky.net"); | ||
``` | ||
Calling `SkynetClient` without parameters will use the URL of the current portal that is running the skapp (sky app). | ||
### async upload(file, [options]) | ||
Use the client to upload `file` contents. | ||
`file` (File) - The file to upload. | ||
`options.APIKey` (string) - Optional API key password for authentication. | ||
`options.onUploadProgress` (function) - Optional callback to track progress. | ||
Returns a promise that resolves with a `{ skylink }` or throws `error` on failure. | ||
```javascript | ||
import { SkynetClient } from "skynet-js"; | ||
const onUploadProgress = (progress, { loaded, total }) => { | ||
console.info(`Progress ${Math.round(progress * 100)}%`); | ||
}; | ||
async function uploadExample() { | ||
try { | ||
const client = new SkynetClient(); | ||
const { skylink } = await client.upload(file, { onUploadProgress }); | ||
} catch (error) { | ||
console.log(error); | ||
} | ||
} | ||
``` | ||
With authentication: | ||
```javascript | ||
import { SkynetClient } from "skynet-js"; | ||
async function authenticationExample() { | ||
try { | ||
const client = new SkynetClient("https://my-portal.net"); | ||
const { skylink } = await client.upload(file, { APIKey: "foobar" }); | ||
} catch (error) { | ||
console.log(error); | ||
} | ||
} | ||
``` | ||
### async uploadDirectory(directory, filename, [options]) | ||
Use the client to upload `directory` contents as a `filename`. | ||
`directory` (Object) - Directory map `{ "file1.jpeg": <File>, "subdirectory/file2.jpeg": <File> }` | ||
`filename` (string) - Output file name (directory name). | ||
`options.onUploadProgress` (function) - Optional callback to track progress. | ||
Returns a promise that resolves with a `{ skylink }` or throws `error` on failure. | ||
#### Browser example | ||
```javascript | ||
import { getRelativeFilePath, getRootDirectory, SkynetClient } from "skynet-js"; | ||
// Assume we have a list of files from an input form. | ||
async function uploadDirectoryExample() { | ||
try { | ||
// Get the directory name from the list of files. | ||
// Can also be named manually, i.e. if you build the files yourself | ||
// instead of getting them from an input form. | ||
const filename = getRootDirectory(files[0]); | ||
// Use reduce to build the map of files indexed by filepaths | ||
// (relative from the directory). | ||
const directory = files.reduce((accumulator, file) => { | ||
const path = getRelativeFilePath(file); | ||
return { ...accumulator, [path]: file }; | ||
}, {}); | ||
const client = new SkynetClient(); | ||
const { skylink } = await client.uploadDirectory(directory, filename); | ||
} catch (error) { | ||
console.log(error); | ||
} | ||
} | ||
``` | ||
### download(skylink) | ||
```javascript | ||
import { SkynetClient } from "skynet-js"; | ||
// Assume we have a skylink e.g. from a previous upload. | ||
try { | ||
const client = new SkynetClient(); | ||
client.download(skylink); | ||
} catch (error) { | ||
console.log(error); | ||
} | ||
``` | ||
Use the client to download `skylink` contents. | ||
`skylink` (string) - 46 character skylink. | ||
Returns nothing. | ||
### open(skylink) | ||
```javascript | ||
import { SkynetClient } from "skynet-js"; | ||
``` | ||
Use the client to open `skylink` in a new browser tab. Browsers support opening natively only limited file extensions like .html or .jpg and will fallback to downloading the file. | ||
`skylink` (string) - 46 character skylink. | ||
Returns nothing. | ||
### getDownloadUrl(skylink, [options]) | ||
```javascript | ||
import { SkynetClient } from "skynet-js"; | ||
``` | ||
Use the client to generate direct `skylink` url. | ||
`skylink` (string) - 46 character skylink. | ||
`options.download` (boolean) - Option to include download directive in the url that will force a download when used. Defaults to `false`. | ||
### parseSkylink(skylink) | ||
```javascript | ||
import { parseSkylink } from "skynet-js"; | ||
``` | ||
Use the `parseSkylink` to extract skylink from a string. | ||
Currently supported string types are: | ||
- direct skylink string, for example `"XABvi7JtJbQSMAcDwnUnmp2FKDPjg8_tTTFP4BwMSxVdEg"` | ||
- `sia:` prefixed string, for example `"sia:XABvi7JtJbQSMAcDwnUnmp2FKDPjg8_tTTFP4BwMSxVdEg"` | ||
- `sia://` prefixed string, for example `"sia://XABvi7JtJbQSMAcDwnUnmp2FKDPjg8_tTTFP4BwMSxVdEg"` | ||
- skylink from url, for example `"https://siasky.net/XABvi7JtJbQSMAcDwnUnmp2FKDPjg8_tTTFP4BwMSxVdEg"` | ||
`skylink` (string) - String containing 46 character skylink. | ||
Returns extracted skylink string or throws error. | ||
`skynet-js` provides functions that only make sense in the browser, and are covered in the special section [Browser JS Utilities](https://nebulouslabs.github.io/skynet-docs/?javascript--browser#browser-js-utilities). |
@@ -1,10 +0,51 @@ | ||
import { defaultPortalUrl } from "./utils.js"; | ||
import axios from "axios"; | ||
import { addUrlQuery, defaultPortalUrl, makeUrl } from "./utils.js"; | ||
export class SkynetClient { | ||
constructor(portalUrl = null) { | ||
if (portalUrl === null) { | ||
portalUrl = defaultPortalUrl(); | ||
} | ||
/** | ||
* The Skynet Client which can be used to access Skynet. | ||
* @constructor | ||
* @param {string} [portalUrl="https://siasky.net"] - The portal URL to use to access Skynet, if specified. To use the default portal while passing custom options, use "". | ||
* @param {Object} [customOptions={}] - Configuration for the client. | ||
* @param {string} [customOptions.method] - HTTP method to use. | ||
* @param {string} [customOptions.APIKey] - Authentication password to use. | ||
* @param {string} [customOptions.customUserAgent=""] - Custom user agent header to set. | ||
* @param {Object} [customOptions.data=null] - Data to send in a POST. | ||
* @param {string} [customOptions.endpointPath=""] - The relative URL path of the portal endpoint to contact. | ||
* @param {string} [customOptions.extraPath=""] - Extra path element to append to the URL. | ||
* @param {Function} [customOptions.onUploadProgress] - Optional callback to track progress. | ||
* @param {Object} [customOptions.query={}] - Query parameters to include in the URl. | ||
*/ | ||
constructor(portalUrl = defaultPortalUrl(), customOptions = {}) { | ||
this.portalUrl = portalUrl; | ||
this.customOptions = customOptions; | ||
} | ||
/** | ||
* Creates and executes a request. | ||
* @param {Object} config - Configuration for the request. See docs for constructor for the full list of options. | ||
*/ | ||
executeRequest(config) { | ||
let url = config.url; | ||
if (!url) { | ||
url = makeUrl(this.portalUrl, config.endpointPath, config.extraPath ?? ""); | ||
url = addUrlQuery(url, config.query); | ||
} | ||
return axios({ | ||
url: url, | ||
method: config.method, | ||
data: config.data, | ||
headers: config.customUserAgent && { "User-Agent": config.customUserAgent }, | ||
auth: config.APIKey && { username: "", password: config.APIKey }, | ||
onUploadProgress: | ||
config.onUploadProgress && | ||
function ({ loaded, total }) { | ||
const progress = loaded / total; | ||
config.onUploadProgress(progress, { loaded, total }); | ||
}, | ||
}); | ||
} | ||
} |
/* eslint-disable no-unused-vars */ | ||
import axios from "axios"; | ||
import { SkynetClient } from "./client.js"; | ||
import { makeUrlWithSkylink, defaultOptions } from "./utils.js"; | ||
import { | ||
addUrlQuery, | ||
defaultOptions, | ||
makeUrl, | ||
parseSkylink, | ||
trimUriPrefix, | ||
uriHandshakePrefix, | ||
uriHandshakeResolverPrefix, | ||
} from "./utils.js"; | ||
@@ -9,28 +19,110 @@ const defaultDownloadOptions = { | ||
}; | ||
const defaultDownloadHnsOptions = { | ||
...defaultOptions("/hns"), | ||
}; | ||
const defaultResolveHnsOptions = { | ||
...defaultOptions("/hnsres"), | ||
}; | ||
SkynetClient.prototype.download = function (skylink, customOptions = {}) { | ||
const opts = { ...defaultDownloadOptions, ...customOptions, download: true }; | ||
const url = this.getDownloadUrl(skylink, opts); | ||
/** | ||
* Initiates a download of the content of the skylink within the browser. | ||
* @param {string} skylink - 46 character skylink. | ||
* @param {Object} [customOptions={}] - Additional settings that can optionally be set. | ||
* @param {string} [customOptions.endpointPath="/"] - The relative URL path of the portal endpoint to contact. | ||
*/ | ||
SkynetClient.prototype.downloadFile = function (skylink, customOptions = {}) { | ||
const opts = { ...defaultDownloadOptions, ...this.customOptions, ...customOptions, download: true }; | ||
const url = this.getSkylinkUrl(skylink, opts); | ||
window.open(url, "_blank"); | ||
// Download the url. | ||
window.location = url; | ||
}; | ||
SkynetClient.prototype.getDownloadUrl = function (skylink, customOptions = {}) { | ||
const opts = { ...defaultDownloadOptions, ...customOptions }; | ||
/** | ||
* Initiates a download of the content of the skylink at the Handshake domain. | ||
* @param {string} domain - Handshake domain. | ||
* @param {Object} [customOptions={}] - Additional settings that can optionally be set. | ||
* @param {string} [customOptions.endpointPath="/hns"] - The relative URL path of the portal endpoint to contact. | ||
*/ | ||
SkynetClient.prototype.downloadFileHns = async function (domain, customOptions = {}) { | ||
const opts = { ...defaultDownloadHnsOptions, ...this.customOptions, ...customOptions, download: true }; | ||
const url = this.getHnsUrl(domain, opts); | ||
// Download the url. | ||
window.location = url; | ||
}; | ||
SkynetClient.prototype.getSkylinkUrl = function (skylink, customOptions = {}) { | ||
const opts = { ...defaultDownloadOptions, ...this.customOptions, ...customOptions }; | ||
const query = opts.download ? { attachment: true } : {}; | ||
return makeUrlWithSkylink(this.portalUrl, opts.endpointPath, skylink, query); | ||
const url = makeUrl(this.portalUrl, opts.endpointPath, parseSkylink(skylink)); | ||
return addUrlQuery(url, query); | ||
}; | ||
SkynetClient.prototype.metadata = function (skylink, customOptions = {}) { | ||
const opts = { ...defaultDownloadOptions, ...customOptions }; | ||
SkynetClient.prototype.getHnsUrl = function (domain, customOptions = {}) { | ||
const opts = { ...defaultDownloadHnsOptions, ...this.customOptions, ...customOptions }; | ||
const query = opts.download ? { attachment: true } : {}; | ||
const url = makeUrl(this.portalUrl, opts.endpointPath, trimUriPrefix(domain, uriHandshakePrefix)); | ||
return addUrlQuery(url, query); | ||
}; | ||
SkynetClient.prototype.getHnsresUrl = function (domain, customOptions = {}) { | ||
const opts = { ...defaultResolveHnsOptions, ...this.customOptions, ...customOptions }; | ||
const query = opts.download ? { attachment: true } : {}; | ||
return makeUrl(this.portalUrl, opts.endpointPath, trimUriPrefix(domain, uriHandshakeResolverPrefix)); | ||
}; | ||
SkynetClient.prototype.getMetadata = async function (skylink, customOptions = {}) { | ||
const opts = { ...defaultDownloadOptions, ...this.customOptions, ...customOptions }; | ||
throw new Error("Unimplemented"); | ||
}; | ||
SkynetClient.prototype.open = function (skylink, customOptions = {}) { | ||
const opts = { ...defaultDownloadOptions, ...customOptions }; | ||
const url = makeUrlWithSkylink(this.portalUrl, opts.endpointPath, skylink); | ||
/** | ||
* Opens the content of the skylink within the browser. | ||
* @param {string} skylink - 46 character skylink. | ||
* @param {Object} [customOptions={}] - Additional settings that can optionally be set. | ||
* @param {string} [customOptions.endpointPath="/"] - The relative URL path of the portal endpoint to contact. | ||
*/ | ||
SkynetClient.prototype.openFile = function (skylink, customOptions = {}) { | ||
const opts = { ...defaultDownloadOptions, ...this.customOptions, ...customOptions }; | ||
const url = this.getSkylinkUrl(skylink, opts); | ||
window.open(url, "_blank"); | ||
}; | ||
/** | ||
* Opens the content of the skylink from the given Handshake domain within the browser. | ||
* @param {string} domain - Handshake domain. | ||
* @param {Object} [customOptions={}] - Additional settings that can optionally be set. | ||
* @param {string} [customOptions.endpointPath="/hns"] - The relative URL path of the portal endpoint to contact. | ||
*/ | ||
SkynetClient.prototype.openFileHns = async function (domain, customOptions = {}) { | ||
const opts = { ...defaultDownloadHnsOptions, ...this.customOptions, ...customOptions }; | ||
const url = this.getHnsUrl(domain, opts); | ||
// Open the url in a new tab. | ||
window.open(url, "_blank"); | ||
}; | ||
/** | ||
* @param {string} domain - Handshake resolver domain. | ||
* @param {Object} [customOptions={}] - Additional settings that can optionally be set. | ||
* @param {string} [customOptions.endpointPath="/hnsres"] - The relative URL path of the portal endpoint to contact. | ||
*/ | ||
SkynetClient.prototype.resolveHns = async function (domain, customOptions = {}) { | ||
const opts = { ...defaultResolveHnsOptions, ...this.customOptions, ...customOptions }; | ||
const url = this.getHnsresUrl(domain, opts); | ||
// Get the txt record from the hnsres domain on the portal. | ||
const response = await this.executeRequest({ | ||
...opts, | ||
method: "get", | ||
url, | ||
}); | ||
return response.data; | ||
}; |
@@ -0,4 +1,12 @@ | ||
import axios from "axios"; | ||
import MockAdapter from "axios-mock-adapter"; | ||
import { SkynetClient, defaultSkynetPortalUrl } from "./index"; | ||
const mock = new MockAdapter(axios); | ||
const portalUrl = defaultSkynetPortalUrl; | ||
const hnsLink = "foo"; | ||
const hnsUrl = `${portalUrl}/hns/${hnsLink}`; | ||
const hnsresUrl = `${portalUrl}/hnsres/${hnsLink}`; | ||
const client = new SkynetClient(portalUrl); | ||
@@ -14,4 +22,6 @@ const skylink = "XABvi7JtJbQSMAcDwnUnmp2FKDPjg8_tTTFP4BwMSxVdEg"; | ||
]; | ||
const validHnsLinkVariations = [hnsLink, `hns:${hnsLink}`, `hns://${hnsLink}`]; | ||
const validHnsresLinkVariations = [hnsLink, `hnsres:${hnsLink}`, `hnsres://${hnsLink}`]; | ||
describe("download", () => { | ||
describe("downloadFile", () => { | ||
it("should call window.open with a download url with attachment set", () => { | ||
@@ -23,6 +33,5 @@ const windowOpen = jest.spyOn(window, "open").mockImplementation(); | ||
client.download(input); | ||
client.downloadFile(input); | ||
expect(windowOpen).toHaveBeenCalledTimes(1); | ||
expect(windowOpen).toHaveBeenCalledWith(`${portalUrl}/${skylink}?attachment=true`, "_blank"); | ||
expect(mock.history.get.length).toBe(0); | ||
}); | ||
@@ -32,13 +41,35 @@ }); | ||
describe("getDownloadUrl", () => { | ||
it("should return correctly formed download URL", () => { | ||
describe("getHnsUrl", () => { | ||
it("should return correctly formed hns URL", () => { | ||
validHnsLinkVariations.forEach((input) => { | ||
expect(client.getHnsUrl(input)).toEqual(hnsUrl); | ||
}); | ||
}); | ||
it("should return correctly formed hns URL with forced download", () => { | ||
const url = client.getHnsUrl(hnsLink, { download: true }); | ||
expect(url).toEqual(`${hnsUrl}?attachment=true`); | ||
}); | ||
}); | ||
describe("getHnsresUrl", () => { | ||
it("should return correctly formed hnsres URL", () => { | ||
validHnsresLinkVariations.forEach((input) => { | ||
expect(client.getHnsresUrl(input)).toEqual(hnsresUrl); | ||
}); | ||
}); | ||
}); | ||
describe("getSkylinkUrl", () => { | ||
it("should return correctly formed skylink URL", () => { | ||
validSkylinkVariations.forEach((input) => { | ||
expect(client.getDownloadUrl(input)).toEqual(`${portalUrl}/${skylink}`); | ||
expect(client.getSkylinkUrl(input)).toEqual(`${portalUrl}/${skylink}`); | ||
}); | ||
}); | ||
it("should return correctly formed url with forced download", () => { | ||
const url = client.getDownloadUrl(skylink, { download: true }); | ||
it("should return correctly formed URL with forced download", () => { | ||
const url = client.getSkylinkUrl(skylink, { download: true, endpointPath: "skynet/skylink" }); | ||
expect(url).toEqual(`${portalUrl}/${skylink}?attachment=true`); | ||
expect(url).toEqual(`${portalUrl}/skynet/skylink/${skylink}?attachment=true`); | ||
}); | ||
@@ -48,3 +79,3 @@ }); | ||
describe("open", () => { | ||
it("should call window.open with a download url", () => { | ||
it("should call window.openFile", () => { | ||
const windowOpen = jest.spyOn(window, "open").mockImplementation(); | ||
@@ -55,3 +86,3 @@ | ||
client.open(input); | ||
client.openFile(input); | ||
@@ -63,1 +94,53 @@ expect(windowOpen).toHaveBeenCalledTimes(1); | ||
}); | ||
describe("downloadFileHns", () => { | ||
it("should set domain with the portal and hns link and then call window.openFile with attachment set", async () => { | ||
const windowOpen = jest.spyOn(window, "open").mockImplementation(); | ||
for (const input of validHnsLinkVariations) { | ||
mock.resetHistory(); | ||
windowOpen.mockReset(); | ||
await client.downloadFileHns(input); | ||
expect(mock.history.get.length).toBe(0); | ||
} | ||
}); | ||
}); | ||
describe("openFileHns", () => { | ||
const hnsUrl = `${portalUrl}/hns/${hnsLink}`; | ||
it("should set domain with the portal and hns link and then call window.openFile", async () => { | ||
const windowOpen = jest.spyOn(window, "open").mockImplementation(); | ||
for (const input of validHnsLinkVariations) { | ||
mock.resetHistory(); | ||
windowOpen.mockReset(); | ||
await client.openFileHns(input); | ||
expect(mock.history.get.length).toBe(0); | ||
expect(windowOpen).toHaveBeenCalledTimes(1); | ||
expect(windowOpen).toHaveBeenCalledWith(hnsUrl, "_blank"); | ||
} | ||
}); | ||
}); | ||
describe("resolveHns", () => { | ||
beforeEach(() => { | ||
mock.onGet(hnsresUrl).reply(200, { skylink: skylink }); | ||
}); | ||
it("should call axios.get with the portal and hnsres link and return the json body", async () => { | ||
for (const input of validHnsresLinkVariations) { | ||
mock.resetHistory(); | ||
const data = await client.resolveHns(input); | ||
expect(mock.history.get.length).toBe(1); | ||
expect(data.skylink).toEqual(skylink); | ||
} | ||
}); | ||
}); |
/* eslint-disable no-unused-vars */ | ||
import { defaultOptions, SkynetClient } from "./utils.js"; | ||
import { SkynetClient } from "./client.js"; | ||
import { defaultOptions } from "./utils.js"; | ||
@@ -5,0 +6,0 @@ const defaultAddSkykeyOptions = { |
@@ -5,2 +5,3 @@ export { SkynetClient } from "./client.js"; | ||
export {} from "./download.js"; | ||
export {} from "./encryption.js"; | ||
export {} from "./upload.js"; | ||
@@ -14,2 +15,5 @@ | ||
parseSkylink, | ||
uriHandshakePrefix, | ||
uriHandshakeResolverPrefix, | ||
uriSkynetPrefix, | ||
} from "./utils.js"; |
@@ -8,10 +8,25 @@ import { SkynetClient } from "./index"; | ||
// Download | ||
expect(client).toHaveProperty("download"); | ||
expect(client).toHaveProperty("getDownloadUrl"); | ||
expect(client).toHaveProperty("open"); | ||
expect(client).toHaveProperty("downloadFile"); | ||
expect(client).toHaveProperty("downloadFileHns"); | ||
expect(client).toHaveProperty("getHnsUrl"); | ||
expect(client).toHaveProperty("getHnsresUrl"); | ||
expect(client).toHaveProperty("getSkylinkUrl"); | ||
expect(client).toHaveProperty("getMetadata"); | ||
expect(client).toHaveProperty("openFile"); | ||
expect(client).toHaveProperty("openFileHns"); | ||
expect(client).toHaveProperty("resolveHns"); | ||
// Encryption | ||
expect(client).toHaveProperty("addSkykey"); | ||
expect(client).toHaveProperty("createSkykey"); | ||
expect(client).toHaveProperty("getSkykeyById"); | ||
expect(client).toHaveProperty("getSkykeyByName"); | ||
expect(client).toHaveProperty("getSkykeys"); | ||
// Upload | ||
expect(client).toHaveProperty("upload"); | ||
expect(client).toHaveProperty("uploadFile"); | ||
expect(client).toHaveProperty("uploadFileRequest"); | ||
expect(client).toHaveProperty("uploadDirectory"); | ||
expect(client).toHaveProperty("uploadDirectoryRequest"); | ||
}); | ||
}); |
@@ -1,5 +0,3 @@ | ||
import axios from "axios"; | ||
import { SkynetClient } from "./client.js"; | ||
import { defaultOptions, makeUrl } from "./utils.js"; | ||
import { defaultOptions, uriSkynetPrefix } from "./utils.js"; | ||
@@ -10,26 +8,24 @@ const defaultUploadOptions = { | ||
portalDirectoryFileFieldname: "files[]", | ||
// TODO: | ||
// customFilename: "", | ||
customFilename: "", | ||
}; | ||
SkynetClient.prototype.upload = async function (file, customOptions = {}) { | ||
const opts = { ...defaultUploadOptions, ...customOptions }; | ||
SkynetClient.prototype.uploadFile = async function (file, customOptions = {}) { | ||
const response = await this.uploadFileRequest(file, customOptions); | ||
return `${uriSkynetPrefix}${response.skylink}`; | ||
}; | ||
SkynetClient.prototype.uploadFileRequest = async function (file, customOptions = {}) { | ||
const opts = { ...defaultUploadOptions, ...this.customOptions, ...customOptions }; | ||
const formData = new FormData(); | ||
formData.append(opts.portalFileFieldname, ensureFileObjectConsistency(file)); | ||
file = ensureFileObjectConsistency(file); | ||
const filename = opts.customFilename ? opts.customFilename : ""; | ||
formData.append(opts.portalFileFieldname, file, filename); | ||
const url = makeUrl(this.portalUrl, opts.endpointPath); | ||
const { data } = await this.executeRequest({ | ||
...opts, | ||
method: "post", | ||
data: formData, | ||
}); | ||
const { data } = await axios.post( | ||
url, | ||
formData, | ||
opts.onUploadProgress && { | ||
onUploadProgress: ({ loaded, total }) => { | ||
const progress = loaded / total; | ||
opts.onUploadProgress(progress, { loaded, total }); | ||
}, | ||
} | ||
); | ||
return data; | ||
@@ -43,27 +39,33 @@ }; | ||
* @param {Object} [customOptions={}] - Additional settings that can optionally be set. | ||
* @param {string} [config.APIKey] - Authentication password to use. | ||
* @param {string} [config.customUserAgent=""] - Custom user agent header to set. | ||
* @param {string} [customOptions.endpointPath="/skynet/skyfile"] - The relative URL path of the portal endpoint to contact. | ||
* @param {Function} [config.onUploadProgress] - Optional callback to track progress. | ||
* @param {string} [customOptions.portalDirectoryfilefieldname="files[]"] - The fieldName for directory files on the portal. | ||
* @returns {Object} data - The returned data. | ||
* @returns {string} data.skylink - The returned skylink. | ||
* @returns {string} data.merkleroot - The hash that is encoded into the skylink. | ||
* @returns {number} data.bitfield - The bitfield that gets encoded into the skylink. | ||
*/ | ||
SkynetClient.prototype.uploadDirectory = async function (directory, filename, customOptions = {}) { | ||
const opts = { ...defaultUploadOptions, ...customOptions }; | ||
const response = await this.uploadDirectoryRequest(directory, filename, customOptions); | ||
return `${uriSkynetPrefix}${response.skylink}`; | ||
}; | ||
SkynetClient.prototype.uploadDirectoryRequest = async function (directory, filename, customOptions = {}) { | ||
const opts = { ...defaultUploadOptions, ...this.customOptions, ...customOptions }; | ||
const formData = new FormData(); | ||
Object.entries(directory).forEach(([path, file]) => { | ||
formData.append(opts.portalDirectoryFileFieldname, ensureFileObjectConsistency(file), path); | ||
file = ensureFileObjectConsistency(file); | ||
formData.append(opts.portalDirectoryFileFieldname, file, path); | ||
}); | ||
const url = makeUrl(this.portalUrl, opts.endpointPath, { filename }); | ||
const { data } = await this.executeRequest({ | ||
...opts, | ||
method: "post", | ||
data: formData, | ||
query: { filename }, | ||
}); | ||
const { data } = await axios.post( | ||
url, | ||
formData, | ||
opts.onUploadProgress && { | ||
onUploadProgress: ({ loaded, total }) => { | ||
const progress = loaded / total; | ||
opts.onUploadProgress(progress, { loaded, total }); | ||
}, | ||
} | ||
); | ||
return data; | ||
@@ -70,0 +72,0 @@ }; |
import axios from "axios"; | ||
import MockAdapter from "axios-mock-adapter"; | ||
import { SkynetClient, defaultSkynetPortalUrl } from "./index"; | ||
import { SkynetClient, defaultSkynetPortalUrl, uriSkynetPrefix } from "./index"; | ||
import { compareFormData } from "./test_utils.js"; | ||
jest.mock("axios"); | ||
const mock = new MockAdapter(axios); | ||
@@ -10,4 +12,6 @@ const portalUrl = defaultSkynetPortalUrl; | ||
const skylink = "XABvi7JtJbQSMAcDwnUnmp2FKDPjg8_tTTFP4BwMSxVdEg"; | ||
const sialink = `${uriSkynetPrefix}${skylink}`; | ||
describe("uploadFile", () => { | ||
const url = `${portalUrl}/skynet/skyfile`; | ||
const filename = "bar.txt"; | ||
@@ -19,82 +23,131 @@ const file = new File(["foo"], filename, { | ||
beforeEach(() => { | ||
axios.post.mockResolvedValue({ data: { skylink } }); | ||
mock.onPost(url).reply(200, { skylink: skylink }); | ||
mock.resetHistory(); | ||
}); | ||
it("should send post request with FormData", () => { | ||
client.upload(file, {}); | ||
it("should send formdata with file", async () => { | ||
const data = await client.uploadFile(file); | ||
expect(axios.post).toHaveBeenCalledWith( | ||
`${portalUrl}/skynet/skyfile`, | ||
expect.any(FormData), // TODO: Inspect data contents. | ||
undefined | ||
); | ||
expect(mock.history.post.length).toBe(1); | ||
const request = mock.history.post[0]; | ||
await compareFormData(request.data, [["file", "foo", filename]]); | ||
expect(data).toEqual(sialink); | ||
}); | ||
it("should send register onUploadProgress callback if defined", () => { | ||
it("should send register onUploadProgress callback if defined", async () => { | ||
const newPortal = "https://my-portal.net"; | ||
const url = `${newPortal}/skynet/skyfile`; | ||
const client = new SkynetClient(newPortal); | ||
client.upload(file, { onUploadProgress: jest.fn() }); | ||
expect(axios.post).toHaveBeenCalledWith( | ||
`${newPortal}/skynet/skyfile`, | ||
expect.any(FormData), // TODO: Inspect data contents. | ||
{ | ||
onUploadProgress: expect.any(Function), | ||
} | ||
); | ||
// Use replyOnce to catch a single request with the new URL. | ||
mock.onPost(url).replyOnce(200, { skylink: skylink }); | ||
const data = await client.uploadFile(file, { onUploadProgress: jest.fn() }); | ||
expect(mock.history.post.length).toBe(1); | ||
const request = mock.history.post[0]; | ||
expect(request.onUploadProgress).toEqual(expect.any(Function)); | ||
await compareFormData(request.data, [["file", "foo", filename]]); | ||
expect(data).toEqual(sialink); | ||
}); | ||
it("should send base-64 authentication password if provided", () => { | ||
client.upload(file, { APIKey: "foobar" }); | ||
it("should use custom filename if provided", async () => { | ||
const data = await client.uploadFile(file, { customFilename: "testname" }); | ||
expect(axios.post).toHaveBeenCalledWith( | ||
`${portalUrl}/skynet/skyfile`, | ||
expect.any(FormData), // TODO: Inspect data contents. | ||
undefined | ||
); | ||
expect(mock.history.post.length).toBe(1); | ||
const request = mock.history.post[0]; | ||
await compareFormData(request.data, [["file", "foo", "testname"]]); | ||
expect(data).toEqual(sialink); | ||
}); | ||
it("should return skylink on success", async () => { | ||
const data = await client.upload(file); | ||
it("should send base-64 authentication password if provided", async () => { | ||
const data = await client.uploadFile(file, { APIKey: "foobar" }); | ||
expect(data).toEqual({ skylink }); | ||
expect(mock.history.post.length).toBe(1); | ||
const request = mock.history.post[0]; | ||
expect(request.auth).toEqual({ username: "", password: "foobar" }); | ||
await compareFormData(request.data, [["file", "foo", filename]]); | ||
expect(data).toEqual(sialink); | ||
}); | ||
it("should send custom user agent if defined", async () => { | ||
const client = new SkynetClient(portalUrl, { customUserAgent: "Sia-Agent" }); | ||
const data = await client.uploadFile(file); | ||
expect(mock.history.post.length).toBe(1); | ||
const request = mock.history.post[0]; | ||
expect(request.headers["User-Agent"]).toEqual("Sia-Agent"); | ||
// Check that other headers weren't altered. | ||
expect(request.headers["Content-Type"]).toEqual("application/x-www-form-urlencoded"); | ||
await compareFormData(request.data, [["file", "foo", filename]]); | ||
expect(data).toEqual(sialink); | ||
}); | ||
it("Should use user agent set in options to function", async () => { | ||
const client = new SkynetClient(portalUrl, { customUserAgent: "Sia-Agent" }); | ||
const data = await client.uploadFile(file, { customUserAgent: "Sia-Agent-2" }); | ||
expect(mock.history.post.length).toBe(1); | ||
const request = mock.history.post[0]; | ||
expect(request.headers["User-Agent"]).toEqual("Sia-Agent-2"); | ||
// Check that other headers weren't altered. | ||
expect(request.headers["Content-Type"]).toEqual("application/x-www-form-urlencoded"); | ||
await compareFormData(request.data, [["file", "foo", filename]]); | ||
expect(data).toEqual(sialink); | ||
}); | ||
}); | ||
describe("uploadDirectory", () => { | ||
const blob = new Blob([], { type: "image/jpeg" }); | ||
const filename = "i-am-root"; | ||
const directory = { | ||
"i-am-not/file1.jpeg": new File([blob], "i-am-not/file1.jpeg"), | ||
"i-am-not/file2.jpeg": new File([blob], "i-am-not/file2.jpeg"), | ||
"i-am-not/me-neither/file3.jpeg": new File([blob], "i-am-not/me-neither/file3.jpeg"), | ||
"i-am-not/file1.jpeg": new File(["foo1"], "i-am-not/file1.jpeg"), | ||
"i-am-not/file2.jpeg": new File(["foo2"], "i-am-not/file2.jpeg"), | ||
"i-am-not/me-neither/file3.jpeg": new File(["foo3"], "i-am-not/me-neither/file3.jpeg"), | ||
}; | ||
const url = `${portalUrl}/skynet/skyfile?filename=${filename}`; | ||
beforeEach(() => { | ||
axios.post.mockResolvedValue({ data: { skylink } }); | ||
mock.onPost(url).reply(200, { skylink: skylink }); | ||
mock.resetHistory(); | ||
}); | ||
it("should send post request with FormData", () => { | ||
client.uploadDirectory(directory, filename); | ||
it("should send formdata with files", async () => { | ||
const data = await client.uploadDirectory(directory, filename); | ||
expect(axios.post).toHaveBeenCalledWith( | ||
`${portalUrl}/skynet/skyfile?filename=${filename}`, | ||
expect.any(FormData), // TODO: Inspect data contents. | ||
undefined | ||
); | ||
}); | ||
expect(mock.history.post.length).toBe(1); | ||
const request = mock.history.post[0]; | ||
it("should send register onUploadProgress callback if defined", () => { | ||
client.uploadDirectory(directory, filename, { onUploadProgress: jest.fn() }); | ||
await compareFormData(request.data, [ | ||
["files[]", "foo1", "i-am-not/file1.jpeg"], | ||
["files[]", "foo2", "i-am-not/file2.jpeg"], | ||
["files[]", "foo3", "i-am-not/me-neither/file3.jpeg"], | ||
]); | ||
expect(axios.post).toHaveBeenCalledWith(`${portalUrl}/skynet/skyfile?filename=${filename}`, expect.any(FormData), { | ||
onUploadProgress: expect.any(Function), | ||
}); | ||
expect(data).toEqual(sialink); | ||
}); | ||
it("should return single skylink on success", async () => { | ||
const data = await client.uploadDirectory(directory, filename); | ||
it("should send register onUploadProgress callback if defined", async () => { | ||
const data = await client.uploadDirectory(directory, filename, { onUploadProgress: jest.fn() }); | ||
expect(data).toEqual({ skylink }); | ||
expect(mock.history.post.length).toBe(1); | ||
const request = mock.history.post[0]; | ||
expect(request.onUploadProgress).toEqual(expect.any(Function)); | ||
expect(data).toEqual(sialink); | ||
}); | ||
}); |
// import axios from "axios"; | ||
import path from "path-browserify"; | ||
import parse from "url-parse"; | ||
import urljoin from "url-join"; | ||
export const defaultSkynetPortalUrl = "https://siasky.net"; | ||
export function defaultPortalUrl() { | ||
var url = new URL(window.location.href); | ||
return url.href.substring(0, url.href.indexOf(url.pathname)); | ||
export const uriHandshakePrefix = "hns:"; | ||
export const uriHandshakeResolverPrefix = "hnsres:"; | ||
export const uriSkynetPrefix = "sia:"; | ||
export function addUrlQuery(url, query) { | ||
const parsed = parse(url); | ||
parsed.set("query", query); | ||
return parsed.toString(); | ||
} | ||
@@ -15,27 +21,15 @@ | ||
endpointPath: endpointPath, | ||
// TODO: | ||
// APIKey: "", | ||
// customUserAgent: "", | ||
APIKey: "", | ||
customUserAgent: "", | ||
}; | ||
} | ||
// TODO: Use this to simplify creating requests. Needs to be tested. | ||
// export function executeRequest(portalUrl, method, opts, query, data = {}) { | ||
// const url = makeUrl(portalUrl, opts.endpointPath, query); | ||
// TODO: This will be smarter. See | ||
// https://github.com/NebulousLabs/skynet-docs/issues/21. | ||
export function defaultPortalUrl() { | ||
var url = new URL(window.location.href); | ||
return url.href.substring(0, url.href.indexOf(url.pathname)); | ||
} | ||
// return axios({ | ||
// method: method, | ||
// url: url, | ||
// data: data, | ||
// auth: opts.APIKey && {username: "", password: opts.APIKey }, | ||
// onUploadProgress: opts.onUploadProgress && { | ||
// onUploadProgress: ({ loaded, total }) => { | ||
// const progress = loaded / total; | ||
// opts.onUploadProgress(progress, { loaded, total }); | ||
// }, | ||
// } | ||
// }); | ||
// } | ||
function getFilePath(file) { | ||
@@ -60,23 +54,19 @@ return file.webkitRelativePath || file.path || file.name; | ||
export function makeUrl(portalUrl, pathname, query = {}) { | ||
const parsed = parse(portalUrl); | ||
parsed.set("pathname", pathname); | ||
parsed.set("query", query); | ||
return parsed.toString(); | ||
/** | ||
* Properly joins paths together to create a URL. Takes a variable number of | ||
* arguments. | ||
* @returns {string} url - The URL. | ||
*/ | ||
export function makeUrl(...args) { | ||
return args.reduce(function (acc, cur) { | ||
return urljoin(acc, cur); | ||
}); | ||
} | ||
export function makeUrlWithSkylink(portalUrl, endpointPath, skylink, query = {}) { | ||
const parsedSkylink = parseSkylink(skylink); | ||
return makeUrl(portalUrl, path.posix.join(endpointPath, parsedSkylink), query); | ||
} | ||
const SKYLINK_MATCHER = "([a-zA-Z0-9_-]{46})"; | ||
const SKYLINK_DIRECT_REGEX = new RegExp(`^${SKYLINK_MATCHER}$`); | ||
const SKYLINK_SIA_PREFIXED_REGEX = new RegExp(`^sia:(?://)?${SKYLINK_MATCHER}$`); | ||
const SKYLINK_PATHNAME_REGEX = new RegExp(`^/${SKYLINK_MATCHER}`); | ||
const SKYLINK_REGEXP_MATCH_POSITION = 1; | ||
export function parseSkylink(skylink = "") { | ||
export function parseSkylink(skylink) { | ||
if (typeof skylink !== "string") throw new Error(`Skylink has to be a string, ${typeof skylink} provided`); | ||
@@ -91,4 +81,3 @@ | ||
// example: sia://XABvi7JtJbQSMAcDwnUnmp2FKDPjg8_tTTFP4BwMSxVdEg | ||
const matchSiaPrefixed = skylink.match(SKYLINK_SIA_PREFIXED_REGEX); | ||
if (matchSiaPrefixed) return matchSiaPrefixed[SKYLINK_REGEXP_MATCH_POSITION]; | ||
skylink = trimUriPrefix(skylink, uriSkynetPrefix); | ||
@@ -103,1 +92,14 @@ // check for skylink passed in an url and extract it | ||
} | ||
export function trimUriPrefix(str, prefix) { | ||
const longPrefix = `${prefix}//`; | ||
if (str.startsWith(longPrefix)) { | ||
// longPrefix is exactly at the beginning | ||
return str.slice(longPrefix.length); | ||
} | ||
if (str.startsWith(prefix)) { | ||
// else prefix is exactly at the beginning | ||
return str.slice(prefix.length); | ||
} | ||
return str; | ||
} |
@@ -1,30 +0,62 @@ | ||
import { makeUrl, makeUrlWithSkylink, parseSkylink, defaultSkynetPortalUrl } from "./utils"; | ||
import { | ||
addUrlQuery, | ||
defaultSkynetPortalUrl, | ||
makeUrl, | ||
parseSkylink, | ||
trimUriPrefix, | ||
uriHandshakePrefix, | ||
uriHandshakeResolverPrefix, | ||
} from "./utils"; | ||
const portalUrl = defaultSkynetPortalUrl; | ||
const skylink = "XABvi7JtJbQSMAcDwnUnmp2FKDPjg8_tTTFP4BwMSxVdEg"; | ||
const validSkylinkVariations = [ | ||
skylink, | ||
`sia:${skylink}`, | ||
`sia://${skylink}`, | ||
`${portalUrl}/${skylink}`, | ||
`${portalUrl}/${skylink}/foo/bar`, | ||
`${portalUrl}/${skylink}?foo=bar`, | ||
]; | ||
const hnsLink = "doesn"; | ||
const hnsresLink = "doesn"; | ||
describe("addUrlQuery", () => { | ||
it("should return correctly formed URLs with query parameters", () => { | ||
expect(addUrlQuery(portalUrl, { filename: "test" })).toEqual(`${portalUrl}?filename=test`); | ||
expect(addUrlQuery(`${portalUrl}/path/`, { download: true })).toEqual(`${portalUrl}/path/?download=true`); | ||
expect(addUrlQuery(`${portalUrl}/skynet/`, { foo: 1, bar: 2 })).toEqual(`${portalUrl}/skynet/?foo=1&bar=2`); | ||
expect(addUrlQuery(`${portalUrl}/`, { attachment: true })).toEqual(`${portalUrl}/?attachment=true`); | ||
}); | ||
}); | ||
describe("makeUrl", () => { | ||
it("should return correctly formed URLs", () => { | ||
expect(makeUrl(portalUrl, "/")).toEqual(`${portalUrl}/`); | ||
expect(makeUrl(portalUrl, "/", { attachment: true })).toEqual(`${portalUrl}/?attachment=true`); | ||
expect(makeUrl(portalUrl, "/skynet")).toEqual(`${portalUrl}/skynet`); | ||
expect(makeUrl(portalUrl, "/skynet/")).toEqual(`${portalUrl}/skynet/`); | ||
expect(makeUrl(portalUrl, "/skynet/", { foo: 1, bar: 2 })).toEqual(`${portalUrl}/skynet/?foo=1&bar=2`); | ||
expect(makeUrlWithSkylink(portalUrl, "/", skylink)).toEqual(`${portalUrl}/${skylink}`); | ||
expect(makeUrlWithSkylink(portalUrl, "/skynet", skylink)).toEqual(`${portalUrl}/skynet/${skylink}`); | ||
expect(makeUrlWithSkylink(portalUrl, "/skynet/", skylink)).toEqual(`${portalUrl}/skynet/${skylink}`); | ||
expect(makeUrl(portalUrl, "/", skylink)).toEqual(`${portalUrl}/${skylink}`); | ||
expect(makeUrl(portalUrl, "/skynet", skylink)).toEqual(`${portalUrl}/skynet/${skylink}`); | ||
expect(makeUrl(portalUrl, "//skynet/", skylink)).toEqual(`${portalUrl}/skynet/${skylink}`); | ||
}); | ||
}); | ||
describe("trimUriPrefix", () => { | ||
it("should correctly parse hns prefixed link", () => { | ||
const validHnsLinkVariations = [hnsLink, `hns:${hnsLink}`, `hns://${hnsLink}`]; | ||
const validHnsresLinkVariations = [hnsresLink, `hnsres:${hnsresLink}`, `hnsres://${hnsresLink}`]; | ||
validHnsLinkVariations.forEach((input) => { | ||
expect(trimUriPrefix(input, uriHandshakePrefix)).toEqual(hnsLink); | ||
}); | ||
validHnsresLinkVariations.forEach((input) => { | ||
expect(trimUriPrefix(input, uriHandshakeResolverPrefix)).toEqual(hnsresLink); | ||
}); | ||
}); | ||
}); | ||
describe("parseSkylink", () => { | ||
it("should correctly parse skylink out of different strings", () => { | ||
const validSkylinkVariations = [ | ||
skylink, | ||
`sia:${skylink}`, | ||
`sia://${skylink}`, | ||
`${portalUrl}/${skylink}`, | ||
`${portalUrl}/${skylink}/foo/bar`, | ||
`${portalUrl}/${skylink}?foo=bar`, | ||
]; | ||
validSkylinkVariations.forEach((input) => { | ||
@@ -36,3 +68,3 @@ expect(parseSkylink(input)).toEqual(skylink); | ||
it("should throw on invalid skylink", () => { | ||
expect(() => parseSkylink()).toThrowError("Could not extract skylink from ''"); | ||
expect(() => parseSkylink()).toThrowError("Skylink has to be a string, undefined provided"); | ||
expect(() => parseSkylink(123)).toThrowError("Skylink has to be a string, number provided"); | ||
@@ -39,0 +71,0 @@ expect(() => parseSkylink("123")).toThrowError("Could not extract skylink from '123'"); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Native code
Supply chain riskContains native code (e.g., compiled binaries or shared libraries). Including native code can obscure malicious behavior.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
214647
2444
0
4
10
27
32
3
+ Addedurl-join@^4.0.1
+ Addedaxios@0.20.0(transitive)
+ Addedfollow-redirects@1.15.9(transitive)
+ Addedurl-join@4.0.1(transitive)
- Removedaxios@0.19.2(transitive)
- Removedfollow-redirects@1.5.10(transitive)
Updatedaxios@^0.20.0