Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Photo storage example #355

Merged
merged 16 commits into from Dec 15, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
32 changes: 23 additions & 9 deletions hosting/photo-storage/README.md
@@ -1,21 +1,27 @@
# Photo storage example

The example shows how to store photos on the IC in an asset canister with the `@dfinity/assets` package. The photo storage app is deployed as frontend
in an assets canister which is also used for photo upload.
The example shows how to store photos on the IC in an asset canister with the `@dfinity/assets` package. The photo
storage app is deployed as frontend in an assets canister which is also used for photo upload.
sesi200 marked this conversation as resolved.
Show resolved Hide resolved

## Installation
This example project can be cloned, installed and deployed locally, for learning and testing purposes. The instructions are based on running the example on either macOS or Linux, but when using WSL2 on Windows, the instructions will be the same.

This example project can be cloned, installed and deployed locally, for learning and testing purposes. The instructions
are based on running the example on either macOS or Linux, but when using WSL2 on Windows, the instructions will be the
same.

### Prerequisites

The example project requires the following installed:

- git
- dfx
- npm

git can be installed from various package managers. DFX can be installed following the instructions [here](https://internetcomputer.org/docs/current/developer-docs/build/install-upgrade-remove).
git and npm can be installed from various package managers. DFX can be installed following the
instructions [here](https://internetcomputer.org/docs/current/developer-docs/build/install-upgrade-remove).

### Download the code

Clone the example dapp project:

```bash
Expand All @@ -24,18 +30,20 @@ $ cd examples/hosting/photo-storage
```

## React build

The React frontend is build by running this command:

```bash
npm install --legacy-peer-deps
npm install
npm run build
```
`--legacy-peer-deps` flags is used here since `react-masonry-component` is not updated yet to have React 18 as peer dependency.

## Deployment

The local network is started by running this command:

```bash
$ dfx start --background
$ dfx start --clean --background
```

When the local network is up and running, run this command to deploy the canisters:
Expand All @@ -45,15 +53,21 @@ $ dfx deploy
```

## Authorization

To authorize the identity from this example project on a local network to upload files, it must be authorized first:

```bash
dfx canister call photo-storage authorize '(principal "535yc-uxytb-gfk7h-tny7p-vjkoe-i4krp-3qmcl-uqfgr-cpgej-yqtjq-rqe")'
```

Before deployment on the IC, the hardcoded identity should be replaced by an authentication method e.g. Internet Identity.
Before deployment on the IC, the hardcoded identity (defined in `src/App.js`) should be replaced by an authentication
method e.g. Internet Identity.

## Cats

The example cat stock photos are from [Pexels](https://www.pexels.com/license/).

## License
This project is licensed under the Apache 2.0 license, see LICENSE.md for details. See CONTRIBUTE.md for details about how to contribute to this project.

This project is licensed under the Apache 2.0 license, see LICENSE.md for details. See CONTRIBUTE.md for details about
how to contribute to this project.
10 changes: 6 additions & 4 deletions hosting/photo-storage/package.json
Expand Up @@ -3,12 +3,14 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@dfinity/assets": "^0.13.4",
"@dfinity/agent": "^0.13.4",
"@dfinity/identity": "^0.13.4",
"@dfinity/agent": "^0.14.0",
"@dfinity/assets": "^0.14.0",
"@dfinity/candid": "^0.14.0",
"@dfinity/identity": "^0.14.0",
"@dfinity/principal": "^0.14.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-masonry-component": "^6.3.0",
"react-masonry-css": "^1.0.16",
"react-scripts": "5.0.1"
},
"scripts": {
Expand Down
25 changes: 18 additions & 7 deletions hosting/photo-storage/src/App.css
@@ -1,30 +1,41 @@
.App-wrapper {
overflow: hidden;
width: calc(100% - 10px);
position: relative;
padding: 10px 10px 0 10px;
}

.App-masonry {
margin: 10px 0 0 10px;
width: 100%;
display: flex;
margin-left: -10px;
width: auto;
}

.App-masonry-column {
padding-left: 10px;
background-clip: padding-box;
}

.App-upload {
background: #222222;
color: #ffffff;
font-family: sans-serif;
font-weight: 600;
width: 100%;
height: 200px;
margin-bottom: 10px;
cursor: pointer;
}

.App-upload:hover {
background: #333333;
}

.App-image, .App-upload {
width: calc(25% - 10px);
.App-image {
margin-bottom: 10px;
background: #eeeeee;
}

.App-image img {
display: block;
width: 100%;
}

.App-progress {
Expand Down
83 changes: 61 additions & 22 deletions hosting/photo-storage/src/App.js
Expand Up @@ -2,16 +2,15 @@ import {Ed25519KeyIdentity} from '@dfinity/identity';
import {HttpAgent} from '@dfinity/agent';
import {AssetManager} from '@dfinity/assets';
import {useEffect, useState} from "react";
import Masonry from "react-masonry-component";
import Masonry from "react-masonry-css";
import './App.css';

// Hardcoded principal: 535yc-uxytb-gfk7h-tny7p-vjkoe-i4krp-3qmcl-uqfgr-cpgej-yqtjq-rqe
// Should be replaced with authentication method e.g. Internet Identity when deployed on IC
const identity = Ed25519KeyIdentity.generate(new Uint8Array(Array.from({length: 32}).map(() => 0)));
const identity = Ed25519KeyIdentity.generate(new Uint8Array(Array.from({length: 32}).fill(0)));
const isLocal = !window.location.host.endsWith('ic0.app');
const agent = new HttpAgent({
host: isLocal ? 'http://127.0.0.1:8000' : 'https://ic0.app',
identity,
host: isLocal ? 'http://127.0.0.1:8000' : 'https://ic0.app', identity,
});
if (isLocal) {
agent.fetchRootKey();
Expand All @@ -23,16 +22,42 @@ const canisterId = new URLSearchParams(window.location.search).get('canisterId')
// Create asset manager instance for above asset canister
const assetManager = new AssetManager({canisterId, agent});

// Get file name, width and height from key
const detailsFromKey = (key) => {
const fileName = key.split('/').slice(-1)[0];
const width = parseInt(fileName.split('.').slice(-3)[0]);
const height = parseInt(fileName.split('.').slice(-2)[0]);
return {key, fileName, width, height}
}

// Get file name, width and height from file
const detailsFromFile = async (file) => {
const src = await new Promise((resolve) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.readAsDataURL(file);
})
const [width, height] = await new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve([img.naturalWidth, img.naturalHeight]);
img.src = src;
})
const name = file.name.split('.');
const extension = name.pop();
const fileName = [name, width, height, extension].join('.');
return {fileName, width, height}
}

const App = () => {
const [uploads, setUploads] = useState([]);
const [progress, setProgress] = useState(1);
const [progress, setProgress] = useState(null);

useEffect(() => {
assetManager.list()
.then(assets => assets
.filter(asset => asset.key.startsWith('/uploads/'))
.sort((a, b) => Number(b.encodings[0].modified - a.encodings[0].modified))
.map(asset => asset.key)
)
.map(({key}) => detailsFromKey(key)))
.then(setUploads);
}, []);

Expand All @@ -42,26 +67,40 @@ const App = () => {
input.accept = 'image/*';
input.multiple = true;
input.onchange = async () => {
const batch = assetManager.batch();
const keys = await Promise.all(Array.from(input.files).map(file => batch.store(file, {path: '/uploads'})));
await batch.commit({onProgress: ({current, total}) => setProgress(current / total)});
setUploads(prevState => [...keys, ...prevState])
setProgress(0);
try {
const batch = assetManager.batch();
const items = await Promise.all(Array.from(input.files).map(async (file) => {
const {fileName, width, height} = await detailsFromFile(file);
const key = await batch.store(file, {path: '/uploads', fileName});
return {key, fileName, width, height};
}));
await batch.commit({onProgress: ({current, total}) => setProgress(current / total)});
setUploads(prevState => [...items, ...prevState])
} catch (e) {
if (e.message.includes('Caller is not authorized')) {
alert("Caller is not authorized, follow Authorization instructions in README");
} else {
throw e;
}
}
setProgress(null)
};
input.click();
}

return (
<>
<div className={'App-wrapper'}>
<Masonry options={{transitionDuration: 0, columnWidth: '.App-image', gutter: 10}}
className={'App-masonry'}>
<button className={'App-upload'} onClick={uploadPhotos}>Upload photo</button>
{uploads.map(upload => <img src={upload} className={'App-image'}
alt={upload.split('/').slice(-1)[0]}/>)}
</Masonry>
</div>
{progress < 1 && <div className={'App-progress'}>{Math.round(progress * 100)}%</div>}
</>
<div className={'App-wrapper'}>
<Masonry breakpointCols={{default: 4, 600: 2, 800: 3}} className={'App-masonry'}
columnClassName="App-masonry-column">
<button className={'App-upload'} onClick={uploadPhotos}>📂 Upload photo</button>
{uploads.map(upload => (
<div key={upload.key} className={'App-image'} style={{aspectRatio: upload.width / upload.height}}>
<img src={upload.key} alt={upload.fileName} loading={'lazy'}/>
</div>))}
</Masonry>
{progress !== null && <div className={'App-progress'}>{Math.round(progress * 100)}%</div>}
</div>
);
}

Expand Down