Skip to content

Commit

Permalink
feat: download original media (#26)
Browse files Browse the repository at this point in the history
* feat(web): get thumbnail on items/get pages

* feat(web): download media as blob
  • Loading branch information
KazuyaHara authored Nov 12, 2022
1 parent 9fd687d commit 889eeb7
Show file tree
Hide file tree
Showing 14 changed files with 113 additions and 36 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@
A gateway to the Glacier.

[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier)

### Firebase Storage CORS setting

To download data directly in the browser, you must configure your Cloud Storage bucket for cross-origin access (CORS). This can be done with the gsutil command line tool, which you can install from [here](https://cloud.google.com/storage/docs/gsutil_install).

If you don't want any domain-based restrictions (the most common scenario), run `gsutil cors set storage.cors.json gs://<your-cloud-storage-bucket>`.
1 change: 1 addition & 0 deletions storage.cors.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{ "origin": ["*"], "method": ["GET"], "maxAgeSeconds": 3600 }]
2 changes: 2 additions & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"date-fns": "^2.29.3",
"file-saver": "^2.0.5",
"firebase": "^9.12.1",
"react": "^18.2.0",
"react-div-100vh": "^0.7.0",
Expand All @@ -61,6 +62,7 @@
"zustand": "^4.1.2"
},
"devDependencies": {
"@types/file-saver": "^2.0.5",
"@typescript-eslint/eslint-plugin": "^5.40.1",
"@typescript-eslint/parser": "^5.40.1",
"eslint": "^7.32.0 || ^8.2.0",
Expand Down
4 changes: 4 additions & 0 deletions web/src/adapters/infrastructure/firebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ export const handleFirestoreError = (error: FirestoreError): Error => {

export const handleStorageError = (error: StorageError) => {
switch (error.code) {
case 'storage/canceled':
return new Error('キャンセルされました');
case 'storage/object-not-found':
return new Error('オブジェクトが見つかりませんでした');
case 'storage/retry-limit-exceeded':
return new Error('最大時間制限を超えました');
case 'storage/unauthenticated':
Expand Down
14 changes: 12 additions & 2 deletions web/src/adapters/infrastructure/medium/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
import { getDownloadURL, ref, uploadBytes, UploadResult } from 'firebase/storage';
import {
getBlob as firebaseGetBlob,
getDownloadURL,
ref,
uploadBytes,
UploadResult,
} from 'firebase/storage';

import { Medium } from '../../../domains/medium';
import Firebase, { handleStorageError } from '../firebase';

export interface IMediumDriver {
getBlob(path: string): Promise<Blob>;
getURL(path: string): Promise<string>;
upload(medium: Medium, data: File): Promise<UploadResult>;
}

export default function mediumDriver(): IMediumDriver {
const getBlob = async (path: string): Promise<Blob> =>
firebaseGetBlob(ref(Firebase.instance.storage, path));

const getURL = async (path: string): Promise<string> =>
getDownloadURL(ref(Firebase.instance.storage, path));

Expand All @@ -20,5 +30,5 @@ export default function mediumDriver(): IMediumDriver {
throw handleStorageError(error);
});

return { getURL, upload };
return { getBlob, getURL, upload };
}
4 changes: 3 additions & 1 deletion web/src/adapters/repositories/medium/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { IMediumRepository } from '../../../interface/repository/medium';
import mediumDriver from '../../infrastructure/medium';

export default function mediumRepository(): IMediumRepository {
const getBlob = async (path: string): Promise<Blob> => mediumDriver().getBlob(path);

const getURL = async (path: string): Promise<string> => mediumDriver().getURL(path);

const upload = async (medium: Medium, file: File): Promise<Medium> => {
const result = await mediumDriver().upload(medium, file);
return { ...medium, path: result.ref.fullPath, thumbnail: result.ref.fullPath };
};

return { getURL, upload };
return { getBlob, getURL, upload };
}
48 changes: 40 additions & 8 deletions web/src/adapters/userInterface/components/pages/item/get/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import React, { useEffect, useState } from 'react';

import { CloudDownload } from '@mui/icons-material';
import { LoadingButton } from '@mui/lab';
import { Box, Button } from '@mui/material';
import { saveAs } from 'file-saver';
import { Navigate, useNavigate, useParams } from 'react-router-dom';

import useItemUseCase from '../../../../../../application/useCases/item';
import useMediumUseCase from '../../../../../../application/useCases/medium';
import { ItemWithURL } from '../../../../../../domains/item';
import itemRepository from '../../../../../repositories/item';
import mediumRepository from '../../../../../repositories/medium';
Expand All @@ -16,6 +20,7 @@ export default function ItemGet() {
itemRepository(),
mediumRepository()
);
const { getBlob } = useMediumUseCase(mediumRepository());
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
const [item, setItem] = useState<ItemWithURL | null>();
Expand Down Expand Up @@ -49,19 +54,46 @@ export default function ItemGet() {
});
};

const onSave = async () => {
if (!item) return navigate('/media');

setLoading(true);
return getBlob(item.medium.path)
.then((blob) => saveAs(blob, item.medium.name))
.catch(({ message }: Error) => {
setLoading(false);
useAlertStore.setState({ message, open: true, severity: 'error' });
})
.finally(() => setLoading(false));
};

const toggleDialog = () => setOpenDialog(!openDialog);

if (typeof item === 'undefined') return <Loading />;
if (!item) return <Navigate to="/media" />;
return (
<Box pb={3}>
<Box component="img" src={item.url} />
<Box display="flex" justifyContent="flex-end" mt={3}>
<Button color="error" disabled={loading} onClick={toggleDialog} size="small">
このメディアを削除する
</Button>
<>
<Box display="flex" justifyContent="flex-end" mb={3}>
<LoadingButton
disableElevation
loading={loading}
onClick={onSave}
startIcon={<CloudDownload />}
sx={{ borderRadius: 2 }}
variant="contained"
>
ダウンロード
</LoadingButton>
</Box>
<Box pb={3}>
<Box component="img" src={item.url} />
<Box display="flex" justifyContent="flex-end" mt={3}>
<Button color="error" disabled={loading} onClick={toggleDialog} size="small">
このメディアを削除
</Button>
</Box>
<Dialog onClose={toggleDialog} onSubmit={onDelete} open={openDialog} />
</Box>
<Dialog onClose={toggleDialog} onSubmit={onDelete} open={openDialog} />
</Box>
</>
);
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import React, { useEffect, useState } from 'react';

import { CloudUpload } from '@mui/icons-material';
import { Box, Button } from '@mui/material';
import { Link } from 'react-router-dom';

import useItemUseCase from '../../../../../../application/useCases/item';
import { ItemWithURL } from '../../../../../../domains/item';
import itemRepository from '../../../../../repositories/item';
import mediumRepository from '../../../../../repositories/medium';
import ItemSectionList from '../../../organisms/sectionList/item';
import Loading from '../../loading';
import Header from '../header';

export default function ItemList() {
const { subscribe } = useItemUseCase(itemRepository(), mediumRepository());
Expand All @@ -21,7 +24,18 @@ export default function ItemList() {
if (typeof items === 'undefined') return <Loading />;
return (
<>
<Header />
<Box display="flex" justifyContent="flex-end" mb={3}>
<Button
component={Link}
disableElevation
startIcon={<CloudUpload />}
sx={{ borderRadius: 2 }}
to="/media/add"
variant="contained"
>
メディアをアップロードする
</Button>
</Box>
<ItemSectionList items={items} sx={{ mt: 3 }} />
</>
);
Expand Down
2 changes: 1 addition & 1 deletion web/src/application/useCases/item/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default function useItemUseCase(
const get = async (id: string) =>
itemRepository.get(id).then(async (item) => {
if (!item) return null;
const url = await mediumRepository.getURL(item.medium.path);
const url = await mediumRepository.getURL(item.medium.thumbnail || item.medium.path);
return { ...item, url };
});

Expand Down
8 changes: 8 additions & 0 deletions web/src/application/useCases/medium/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { IMediumRepository } from '../../../interface/repository/medium';
import { IMediumUseCase } from '../../../interface/useCase/medium';

export default function useMediumUseCase(mediumRepository: IMediumRepository): IMediumUseCase {
const getBlob = async (path: string) => mediumRepository.getBlob(path);

return { getBlob };
}
1 change: 1 addition & 0 deletions web/src/interface/repository/medium/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Medium } from '../../../domains/medium';

export interface IMediumRepository {
getBlob(path: string): Promise<Blob>;
getURL(path: string): Promise<string>;
upload(medium: Medium, file: File): Promise<Medium>;
}
3 changes: 3 additions & 0 deletions web/src/interface/useCase/medium/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface IMediumUseCase {
getBlob(path: string): Promise<Blob>;
}
16 changes: 16 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4638,6 +4638,13 @@ __metadata:
languageName: node
linkType: hard

"@types/file-saver@npm:^2.0.5":
version: 2.0.5
resolution: "@types/file-saver@npm:2.0.5"
checksum: a31d6ee2abf99598647139f8f35b37b6e1bacf6c7ddf05c66651b9b0e6e53381aa0e8ed13f37faa6e496e0eb1da87c97e6c70fd589d5b83b0c95c57cb64ce92a
languageName: node
linkType: hard

"@types/glob@npm:^7.1.1":
version: 7.2.0
resolution: "@types/glob@npm:7.2.0"
Expand Down Expand Up @@ -9147,6 +9154,13 @@ __metadata:
languageName: node
linkType: hard

"file-saver@npm:^2.0.5":
version: 2.0.5
resolution: "file-saver@npm:2.0.5"
checksum: c62d96e5cebc58b4bdf3ae8a60d5cf9607ad82f75f798c33a4ee63435ac2203002584d5256a2a780eda7feb5e19dc3b6351c2212e58b3f529e63d265a7cc79f7
languageName: node
linkType: hard

"filelist@npm:^1.0.1":
version: 1.0.4
resolution: "filelist@npm:1.0.4"
Expand Down Expand Up @@ -18242,6 +18256,7 @@ __metadata:
"@testing-library/jest-dom": ^5.14.1
"@testing-library/react": ^13.0.0
"@testing-library/user-event": ^13.2.1
"@types/file-saver": ^2.0.5
"@types/jest": ^27.0.1
"@types/node": ^16.7.13
"@types/react": ^18.0.0
Expand All @@ -18257,6 +18272,7 @@ __metadata:
eslint-plugin-jsx-a11y: ^6.5.1
eslint-plugin-react: ^7.28.0
eslint-plugin-react-hooks: ^4.3.0
file-saver: ^2.0.5
firebase: ^9.12.1
prettier-plugin-packagejson: ^2.3.0
react: ^18.2.0
Expand Down

0 comments on commit 889eeb7

Please sign in to comment.