+
+## 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://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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Readable stream example
+
+
Files to add
+
+
+
+
Add file
+
+
+
+
Output
+
+
+
+
+
+
+
+
+
+
Preview files
+
+
+
+
\ 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: ``
+ },
+ {
+ 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