-
Notifications
You must be signed in to change notification settings - Fork 317
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #355 from sea-snake/photo-storage
feat: Photo storage example
- Loading branch information
Showing
18 changed files
with
401 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
name: hosting-photo-storage | ||
on: | ||
push: | ||
branches: | ||
- master | ||
pull_request: | ||
paths: | ||
- hosting/photo-storage/** | ||
- .github/workflows/provision-darwin.sh | ||
- .github/workflows/provision-linux.sh | ||
- .github/workflows/hosting-photo-storage-example.yml | ||
concurrency: | ||
group: ${{ github.workflow }}-${{ github.ref }} | ||
cancel-in-progress: true | ||
jobs: | ||
hosting-photo-storage-darwin: | ||
runs-on: macos-12 | ||
steps: | ||
- uses: actions/checkout@v1 | ||
- name: Provision Darwin | ||
run: bash .github/workflows/provision-darwin.sh | ||
- name: Hosting Photo Storage Darwin | ||
run: | | ||
pushd hosting/photo-storage | ||
# verify frontend deps install and build | ||
npm install | ||
npm run build | ||
# verify that frontend asset canister deploys | ||
dfx start --background | ||
dfx deploy | ||
popd | ||
hosting-photo-storage-linux: | ||
runs-on: ubuntu-20.04 | ||
steps: | ||
- uses: actions/checkout@v1 | ||
- name: Provision Linux | ||
run: bash .github/workflows/provision-linux.sh | ||
- name: Hosting Photo Storage Linux | ||
run: | | ||
pushd hosting/photo-storage | ||
# verify frontend deps install and build | ||
npm install | ||
npm run build | ||
# verify that frontend asset canister deploys | ||
dfx start --background | ||
dfx deploy | ||
popd |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
# 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 a frontend in an asset 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 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 | ||
$ 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 | ||
npm run build | ||
``` | ||
|
||
## Deployment | ||
|
||
The local network is started by running this command: | ||
|
||
```bash | ||
$ dfx start --clean --background | ||
``` | ||
|
||
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")' | ||
``` | ||
|
||
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 file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
{ | ||
"canisters": { | ||
"photo-storage": { | ||
"frontend": { | ||
"entrypoint": "build/index.html" | ||
}, | ||
"source": [ | ||
"build" | ||
], | ||
"type": "assets" | ||
} | ||
}, | ||
"defaults": { | ||
"build": { | ||
"args": "", | ||
"packtool": "" | ||
} | ||
}, | ||
"version": 1 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
{ | ||
"name": "photo-storage", | ||
"version": "0.1.0", | ||
"private": true, | ||
"dependencies": { | ||
"@dfinity/agent": "^0.15.0", | ||
"@dfinity/assets": "^0.15.0", | ||
"@dfinity/candid": "^0.15.0", | ||
"@dfinity/identity": "^0.15.0", | ||
"@dfinity/principal": "^0.15.0", | ||
"react": "^18.2.0", | ||
"react-dom": "^18.2.0", | ||
"react-masonry-css": "^1.0.16", | ||
"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" | ||
] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> |
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
.App-wrapper { | ||
padding: 10px 10px 0 10px; | ||
} | ||
|
||
.App-masonry { | ||
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 { | ||
margin-bottom: 10px; | ||
background: #eeeeee; | ||
} | ||
|
||
.App-image img { | ||
display: block; | ||
width: 100%; | ||
} | ||
|
||
.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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
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-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}).fill(0))); | ||
const isLocal = !window.location.host.endsWith('ic0.app'); | ||
const agent = new HttpAgent({ | ||
host: isLocal ? `http://127.0.0.1:${window.location.port}` : '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}); | ||
|
||
// 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(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(({key}) => detailsFromKey(key))) | ||
.then(setUploads); | ||
}, []); | ||
|
||
const uploadPhotos = () => { | ||
const input = document.createElement('input'); | ||
input.type = 'file'; | ||
input.accept = 'image/*'; | ||
input.multiple = true; | ||
input.onchange = async () => { | ||
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 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> | ||
); | ||
} | ||
|
||
export default App; |
Oops, something went wrong.