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 2 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
23 changes: 23 additions & 0 deletions hosting/photo-storage/.gitignore
@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*
59 changes: 59 additions & 0 deletions hosting/photo-storage/README.md
@@ -0,0 +1,59 @@
# 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.

## 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.

### 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).
sea-snake marked this conversation as resolved.
Show resolved Hide resolved

### Download the code
Clone the example dapp project:

```bash
$ git clone https://github.com/dfinity/examples
$ cd examples/hosting/photo-storage
```

## React build
The React frontend is build by running this command:
```bash
npm install --legacy-peer-deps
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
sea-snake marked this conversation as resolved.
Show resolved Hide resolved
```

When the local network is up and running, run this command to deploy the canisters:

```bash
$ 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")'
```
sea-snake marked this conversation as resolved.
Show resolved Hide resolved

Before deployment on the IC, the hardcoded identity should be replaced by an authentication method e.g. Internet Identity.
sea-snake marked this conversation as resolved.
Show resolved Hide resolved

## 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.
26 changes: 26 additions & 0 deletions hosting/photo-storage/dfx.json
@@ -0,0 +1,26 @@
{
"canisters": {
"photo-storage": {
"frontend": {
"entrypoint": "build/index.html"
},
"source": [
"build"
],
"type": "assets"
}
},
"defaults": {
"build": {
"args": "",
"packtool": ""
}
},
"networks": {
"local": {
"bind": "127.0.0.1:8000",
"type": "ephemeral"
}
},
"version": 1
}
38 changes: 38 additions & 0 deletions hosting/photo-storage/package.json
@@ -0,0 +1,38 @@
{
"name": "photo-storage",
"version": "0.1.0",
"private": true,
"dependencies": {
"@dfinity/assets": "^0.13.4",
"@dfinity/agent": "^0.13.4",
"@dfinity/identity": "^0.13.4",
sea-snake marked this conversation as resolved.
Show resolved Hide resolved
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-masonry-component": "^6.3.0",
"react-scripts": "5.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
12 changes: 12 additions & 0 deletions hosting/photo-storage/public/index.html
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Photo storage</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
Binary file added hosting/photo-storage/public/uploads/cat1.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added hosting/photo-storage/public/uploads/cat2.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added hosting/photo-storage/public/uploads/cat3.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added hosting/photo-storage/public/uploads/cat4.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added hosting/photo-storage/public/uploads/cat5.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added hosting/photo-storage/public/uploads/cat6.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added hosting/photo-storage/public/uploads/cat7.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added hosting/photo-storage/public/uploads/cat8.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 44 additions & 0 deletions hosting/photo-storage/src/App.css
@@ -0,0 +1,44 @@
.App-wrapper {
overflow: hidden;
width: calc(100% - 10px);
position: relative;
}

.App-masonry {
margin: 10px 0 0 10px;
width: 100%;
}

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

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

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

.App-progress {
position: fixed;
top: 50%;
left: 50%;
margin: -30px 0 0 -50px;
width: 100px;
height: 60px;
background: #222222;
font-family: sans-serif;
text-align: center;
line-height: 60px;
font-weight: 600;
color: #ffffff;
box-shadow: 0 0 0 9999px rgba(255, 255, 255, 0.8);
}
68 changes: 68 additions & 0 deletions hosting/photo-storage/src/App.js
@@ -0,0 +1,68 @@
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 './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 isLocal = !window.location.host.endsWith('ic0.app');
const agent = new HttpAgent({
host: isLocal ? 'http://127.0.0.1:8000' : 'https://ic0.app',
identity,
});
if (isLocal) {
agent.fetchRootKey();
}

// Canister id can be fetched from URL since frontend in this example is hosted in the same canister as file upload
const canisterId = new URLSearchParams(window.location.search).get('canisterId') ?? /(.*?)(?:\.raw)?\.ic0.app/.exec(window.location.host)?.[1] ?? /(.*)\.localhost/.exec(window.location.host)?.[1];

// Create asset manager instance for above asset canister
const assetManager = new AssetManager({canisterId, agent});

const App = () => {
const [uploads, setUploads] = useState([]);
const [progress, setProgress] = useState(1);
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)
)
.then(setUploads);
}, []);

const uploadPhotos = () => {
const input = document.createElement('input');
input.type = 'file';
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])
};
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>}
</>
);
}

export default App;
13 changes: 13 additions & 0 deletions hosting/photo-storage/src/index.css
@@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
11 changes: 11 additions & 0 deletions hosting/photo-storage/src/index.js
@@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);