diff --git a/examples/helia-readable-stream/.gitignore b/examples/helia-readable-stream/.gitignore new file mode 100644 index 00000000..910f6339 --- /dev/null +++ b/examples/helia-readable-stream/.gitignore @@ -0,0 +1,8 @@ +node_modules +build +dist +.docs +.coverage +node_modules +package-lock.json +yarn.lock diff --git a/examples/helia-readable-stream/README.md b/examples/helia-readable-stream/README.md new file mode 100644 index 00000000..d4e60459 --- /dev/null +++ b/examples/helia-readable-stream/README.md @@ -0,0 +1,119 @@ +

+ + IPFS in JavaScript logo + +

+ +

Readable Stream

+ +

+ Using duplex streams to add files to IPFS in the browser +
+
+ +
+ Explore the docs + · + View Demo + · + Report Bug + · + Request Feature/Example +

+ +## Table of Contents + +- [Table of Contents](#table-of-contents) +- [About The Project](#about-the-project) +- [Getting Started](#getting-started) + - [Prerequisites](#prerequisites) + - [Installation and Running example](#installation-and-running-example) +- [Usage](#usage) +- [References](#references) +- [Documentation](#documentation) +- [Contributing](#contributing) +- [Want to hack on IPFS?](#want-to-hack-on-ipfs) + +## About The Project + +- Read the [docs](https://github.com/ipfs/js-ipfs/tree/master/docs) +- Look into other [examples](https://github.com/ipfs-examples/js-ipfs-examples) to learn how to spawn an IPFS node in Node.js and in the Browser +- Consult the [Core API docs](https://github.com/ipfs/js-ipfs/tree/master/docs/core-api) to see what you can do with an IPFS node +- Visit https://dweb-primer.ipfs.io to learn about IPFS and the concepts that underpin it +- Head over to https://proto.school to take interactive tutorials that cover core IPFS APIs +- Check out https://docs.ipfs.io for tips, how-tos and more +- See https://blog.ipfs.io for news and more +- Need help? Please ask 'How do I?' questions on https://discuss.ipfs.io + +## Getting Started + +### Prerequisites + +Make sure you have installed all of the following prerequisites on your development machine: + +- Git - [Download & Install Git](https://git-scm.com/downloads). OSX and Linux machines typically have this already installed. +- Node.js - [Download & Install Node.js](https://nodejs.org/en/download/) and the npm package manager. + +### Installation and Running example + +```console +> npm install +> npm start +``` + +Now open your browser at `http://localhost:8888` + +## Usage + +If you have a number of files that you'd like to add to IPFS and end up with a hash representing the directory containing your files, you can invoke [`ipfs.add`](https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/SPEC/FILES.md#add) with an array of objects. + +But what if you don't know how many there will be in advance? You can add multiple files to a directory in IPFS over time by using [`ipfs.addReadableStream`](https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/SPEC/FILES.md#addreadablestream). + +This example demonstrates the `Regular API`, top-level API for add, cat, get and ls Files on IPFS + +_For more examples, please refer to the [Documentation](#documentation)_ + +## References + +- Documentation: + - [IPFS CONFIG](https://github.com/ipfs/js-ipfs/blob/master/docs/CONFIG.md) + - [MISCELLANEOUS](https://github.com/ipfs/js-ipfs/blob/master/docs/core-api/MISCELLANEOUS.md) + - [FILES](https://github.com/ipfs/js-ipfs/blob/master/docs/core-api/FILES.md) +- Tutorials: + - [MFS API](https://proto.school/mutable-file-system) + - [Regular File API](https://proto.school/regular-files-api) + +## Documentation + +- [Config](https://docs.ipfs.io/) +- [Core API](https://github.com/ipfs/js-ipfs/tree/master/docs/core-api) +- [Examples](https://github.com/ipfs-examples/js-ipfs-examples) +- [Development](https://github.com/ipfs/js-ipfs/blob/master/docs/DEVELOPMENT.md) +- [Tutorials](https://proto.school) + +## Contributing + +Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. + +1. Fork the IPFS Project +2. Create your Feature Branch (`git checkout -b feature/amazing-feature`) +3. Commit your Changes (`git commit -a -m 'feat: add some amazing feature'`) +4. Push to the Branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +## Want to hack on IPFS? + +[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) + +The IPFS implementation in JavaScript needs your help! There are a few things you can do right now to help out: + +Read the [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md) and [JavaScript Contributing Guidelines](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md). + +- **Check out existing issues** The [issue list](https://github.com/ipfs/js-ipfs/issues) has many that are marked as ['help wanted'](https://github.com/ipfs/js-ipfs/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22help+wanted%22) or ['difficulty:easy'](https://github.com/ipfs/js-ipfs/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Adifficulty%3Aeasy) which make great starting points for development, many of which can be tackled with no prior IPFS knowledge +- **Look at the [IPFS Roadmap](https://github.com/ipfs/roadmap)** This are the high priority items being worked on right now +- **Perform code reviews** More eyes will help + a. speed the project along + b. ensure quality, and + c. reduce possible future bugs. +- **Add tests**. There can never be enough tests. +- **Join the [Weekly Core Implementations Call](https://github.com/ipfs/team-mgmt/issues/992)** it's where everyone discusses what's going on with IPFS and what's next \ No newline at end of file diff --git a/examples/helia-readable-stream/index.html b/examples/helia-readable-stream/index.html new file mode 100644 index 00000000..4232ddf9 --- /dev/null +++ b/examples/helia-readable-stream/index.html @@ -0,0 +1,113 @@ + + + + + + + Add readable stream + + + + + + + + + + +
+ + IPFS logo + +
+ +
+

Readable stream example

+ +

Files to add

+ +
+ +

Add file

+ +
+ + + + + + + + + + +
+ +

Output

+ +
+
+
+
+
+
+ + +
+ + \ No newline at end of file diff --git a/examples/helia-readable-stream/package.json b/examples/helia-readable-stream/package.json new file mode 100644 index 00000000..c72e9afa --- /dev/null +++ b/examples/helia-readable-stream/package.json @@ -0,0 +1,30 @@ +{ + "name": "example-browser-add-readable-stream", + "version": "1.0.0", + "private": true, + "type": "module", + "description": "How to add readable streams in the browser", + "keywords": [], + "license": "MIT", + "scripts": { + "clean": "rimraf ./dist ./.cache ./node_modules/.vite", + "build": "vite build", + "serve": "vite dev --port 8888", + "start": "npm run serve", + "test": "npm run build && playwright test tests" + }, + "browserslist": "last 1 Chrome version", + "dependencies": { + "ipfs-core": "^0.16.0" + }, + "devDependencies": { + "@babel/core": "^7.14.8", + "@playwright/test": "^1.12.3", + "playwright": "^1.12.3", + "process": "^0.11.10", + "rimraf": "^3.0.2", + "test-util-ipfs-example": "^1.0.2", + "util": "^0.12.4", + "vite": "^3.1.0" + } + } \ No newline at end of file diff --git a/examples/helia-readable-stream/src/index.js b/examples/helia-readable-stream/src/index.js new file mode 100644 index 00000000..feb9f5a6 --- /dev/null +++ b/examples/helia-readable-stream/src/index.js @@ -0,0 +1,222 @@ +import { createHelia } from 'helia' +import { unixfs } from '@helia/unixfs' + +const main = async () => { + let helia; + let fs; + + const DOM = { + output: document.getElementById('output'), + examples: document.getElementById('examples'), + content: document.getElementById('content'), + preview: document.getElementById('preview'), + fileDirectory: document.getElementById('file-directory'), + fileName: document.getElementById('file-name'), + fileContent: document.getElementById('file-content'), + addBtn: document.getElementById('add-submit'), + } + + const COLORS = { + active: '#357edd', + success: '#0cb892', + error: '#ea5037' + } + + // content could be a stream, a url, a Uint8Array, a File etc + const examples = [ + { + name: `file1.txt`, + content: 'Hello world! :)' + }, + { + name: `file2.svg`, + content: ` + + Sorry, your browser does not support inline SVG. + ` + }, + { + name: `file3.json`, + content: `{ + text: 'IPFS is awesome' + }` + } + ]; + + const scrollToBottom = () => { + const terminal = document.getElementById('terminal') + + terminal.scroll({ top: terminal.scrollHeight, behavior: 'smooth' }) + } + + const showStatus = (text, bg) => { + console.info(text) + + const log = DOM.output + + if (!log) { + return + } + + const line = document.createElement('p') + line.innerText = text + line.style.color = bg + + log.appendChild(line) + + scrollToBottom(log) + } + + const addFileDOM = (name, content, DOM, codeAsText = false) => { + const details = document.createElement("details"); + const summary = document.createElement("summary"); + summary.textContent = name; + + details.appendChild(summary); + + const code = document.createElement("div") + + if (codeAsText) { + code.textContent = content + } else { + code.innerHTML = content; + } + + details.appendChild(code); + + DOM.appendChild(details); + } + + const createFiles = (directory, files) => { + return files.map(file => { + return { + path: `${directory}/${file.name}`, + content: file.content + } + }) + } + + const streamFiles = async (directory, files) => { + // Create a stream to write files to + const stream = new ReadableStream({ + start(controller) { + for (const file of files) { + // Add the files one by one + controller.enqueue(file) + } + + // When we have no more files to add, close the stream + controller.close() + } + }) + + for await (const data of fs.addAll(stream)) { + // The last data event will contain the directory hash + if (data.path === directory) { + return data.cid + } + } + + throw new Error('Could not find directory in `ipfs.addAll` output') + + return cid + } + + const addFile = async (name, content, directory) => { + if (!helia) { + showStatus(`Creating Helia node...`, COLORS.active) + + const repoPath = `helia-${Math.random()}` + helia = await createHelia({ repo: repoPath }) + fs = unixfs(helia) + } + + const id = helia.libp2p.peerId + showStatus(`Connecting to ${id}...`, COLORS.active) + + const filesToAdd = [...examples]; + console.log(filesToAdd) + if (name != null && content != null) { + filesToAdd.push({path: name, content: content}) + } + + const files = createFiles(directory, filesToAdd) + console.log(files) + showStatus(`Streaming file(s)...`, COLORS.active) + const directoryHash = await streamFiles(directory, files) + + showStatus(`Added to ${directoryHash}`, COLORS.active) + + showStatus(`Listing directory ${directoryHash}...`, COLORS.active) + const fileList = await fs.ls(directoryHash); + + showStatus(`Directory contents:`) + showStatus(`${directory}/ ${directoryHash}`) + + if (!DOM.content) { + const dom = document.createElement("div"); + dom.id = "content" + DOM.preview.appendChild(dom) + + DOM.content = dom + } + + DOM.content.innerHTML = ''; + + for await (const file of fileList) { + const decoder = new TextDecoder() + let content = '' + + for await (const chunk of helia.cat(file.cid)) { + content += decoder.decode(chunk, { + stream: true + }) + } + + showStatus(`\u2514\u2500 ${file.name} ${file.path} ${content}`) + showStatus(`Preview: https://ipfs.io/ipfs/${file.path}`, COLORS.success) + addFileDOM(file.name, content, DOM.content, false) + } + + showStatus(`Done!`, COLORS.success) + + DOM.preview.className = '' + } + + // Log examples + for (const example of examples) { + addFileDOM(example.name, example.content, DOM.examples, true); + } + + // Event listeners + DOM.addBtn.onclick = async (e) => { + e.preventDefault() + const directory = DOM.fileDirectory.value + const name = DOM.fileName.value + const content = DOM.fileContent.value + + try { + if ((name == null || name === '') && content.trim().length > 0) { + showStatus(`Is missing either the 'Name'`, COLORS.error) + return + } + + if ((content == null || content === '') && name.trim().length > 0) { + showStatus(`Is missing either the 'Name'`, COLORS.error) + return + } + + if (name == null || name === '' || content == null || content === '') { + showStatus(`Input file will be ignored`) + await addFile(null, null, directory) + } else { + await addFile(name, content, directory) + } + } catch (err) { + showStatus(err.message, COLORS.error) + console.error(err) + } + } +} + +main() \ No newline at end of file diff --git a/examples/helia-readable-stream/src/style.css b/examples/helia-readable-stream/src/style.css new file mode 100644 index 00000000..0c8ed0c7 --- /dev/null +++ b/examples/helia-readable-stream/src/style.css @@ -0,0 +1,63 @@ +:placeholder { + color: rgb(0 0 0 / 30%); + } + + form { + margin: 1.25rem 0; + } + + .hidden { + display: none; + } + + .window { + display: flex; + flex-direction: column; + background: #222; + color: #fff; + height: 400px; + } + + .window .header { + flex-basis: 3rem; + background: #c6c6c6; + position: relative; + } + + .window .header:after { + content: ". . ."; + position: absolute; + left: 12px; + right: 0; + top: -3px; + font-family: "Times New Roman", Times, serif; + font-size: 96px; + color: #fff; + line-height: 0; + letter-spacing: -12px; + } + + .terminal { + margin: 20px; + font-family: monospace; + font-size: 16px; + overflow: auto; + flex: 1; + } + + details > summary { + background-color: #0b3a53; + color: whitesmoke; + cursor: pointer; + padding: 0.5rem 1rem; + } + + details > summary > * { + display: inline; + } + + details > div { + border: 2px solid #0b3a53; + margin-top: 0; + padding: 1rem; + } \ No newline at end of file diff --git a/examples/helia-readable-stream/vite.config.js b/examples/helia-readable-stream/vite.config.js new file mode 100644 index 00000000..90b3643b --- /dev/null +++ b/examples/helia-readable-stream/vite.config.js @@ -0,0 +1,29 @@ +import { defineConfig } from 'vite' + +export default defineConfig({ + build: { + target: 'es2020', + minify: false, + // disable @rollup/plugin-commonjs https://github.com/vitejs/vite/issues/9703#issuecomment-1216662109 + // should be removable with vite 4 https://vitejs.dev/blog/announcing-vite3.html#esbuild-deps-optimization-at-build-time-experimental + commonjsOptions: { + include: [] + } + }, + define: { + 'process.env.NODE_DEBUG': 'false', + 'global': 'globalThis' + }, + optimizeDeps: { + // enable esbuild dep optimization during build https://github.com/vitejs/vite/issues/9703#issuecomment-1216662109 + // should be removable with vite 4 https://vitejs.dev/blog/announcing-vite3.html#esbuild-deps-optimization-at-build-time-experimental + disabled: false, + + // target: es2020 added as workaround to make big ints work + // - should be removable with vite 4 + // https://github.com/vitejs/vite/issues/9062#issuecomment-1182818044 + esbuildOptions: { + target: 'es2020' + } + } +}) \ No newline at end of file