Files
rowy/src/hooks/useFirebaseStorageUploader.tsx
2022-11-16 11:15:10 +03:00

207 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useReducer } from "react";
import { useAtom } from "jotai";
import { useSnackbar } from "notistack";
import type { DocumentReference } from "firebase/firestore";
import {
ref,
uploadBytesResumable,
deleteObject,
getDownloadURL,
} from "firebase/storage";
import { Paper, Button } from "@mui/material";
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
import { projectScope } from "@src/atoms/projectScope";
import { firebaseStorageAtom } from "@src/sources/ProjectSourceFirebase";
import { WIKI_LINKS } from "@src/constants/externalLinks";
import { FileValue } from "@src/types/table";
import { generateId } from "@src/utils/table";
export type UploadState = {
progress: number;
loading: boolean;
error?: string;
};
export type UploaderState = {
[fileName: string]: UploadState;
};
const uploadReducer = (
prevState: UploaderState,
action: {
type: "reset" | "file_update";
data?: { fileName: string; newProps: Partial<UploadState> };
}
) => {
switch (action.type) {
case "reset":
return {};
case "file_update":
const { fileName, newProps } = action.data!;
const nextState = { ...prevState };
nextState[fileName] = {
...nextState[fileName],
...newProps,
};
return nextState;
}
};
export type UploadProps = {
docRef: DocumentReference;
fieldName: string;
files: File[];
onComplete?: ({
uploads,
failures,
}: {
uploads: FileValue[];
failures: string[];
}) => void;
};
// TODO: GENERALIZE INTO ATOM
const useFirebaseStorageUploader = () => {
const [firebaseStorage] = useAtom(firebaseStorageAtom, projectScope);
const { enqueueSnackbar } = useSnackbar();
const [uploaderState, uploaderDispatch] = useReducer(uploadReducer, {});
const upload = ({ docRef, fieldName, files }: UploadProps) => {
const uploads = [] as FileValue[];
const failures = [] as string[];
const isCompleted = () => uploads.length + failures.length === files.length;
return new Promise((resolve) =>
files.forEach((file) => {
uploaderDispatch({
type: "file_update",
data: {
fileName: file.name,
newProps: { loading: true, progress: 0 },
},
});
const storageRef = ref(
firebaseStorage,
`${docRef.path}/${fieldName}/${generateId()}-${file.name}`
);
const uploadTask = uploadBytesResumable(storageRef, file, {
cacheControl: "public, max-age=31536000",
});
uploadTask.on(
// event
"state_changed",
// observer
(snapshot) => {
// Get task progress, including the number of bytes uploaded and the total number of bytes to be uploaded
const progress =
(snapshot.bytesTransferred / snapshot.totalBytes) * 100;
uploaderDispatch({
type: "file_update",
data: { fileName: file.name, newProps: { progress } },
});
},
// error  must be any to access error.code
(error: any) => {
// A full list of error codes is available at
// https://firebase.google.com/docs/storage/web/handle-errors
const errorAction = {
fileName: file.name,
newProps: { loading: false } as Partial<UploadState>,
};
switch (error.code) {
case "storage/unknown":
// Unknown error occurred, inspect error.serverResponse
enqueueSnackbar("Unknown error occurred", { variant: "error" });
errorAction.newProps.error = error.serverResponse;
break;
case "storage/unauthorized":
// User doesn't have permission to access the object
enqueueSnackbar("You dont have permissions to upload files", {
variant: "error",
action: (
<Paper elevation={0} sx={{ borderRadius: 1 }}>
<Button
color="primary"
href={
WIKI_LINKS.setupRoles +
"#write-firebase-storage-security-rules"
}
target="_blank"
rel="noopener noreferrer"
>
Fix
<InlineOpenInNewIcon />
</Button>
</Paper>
),
});
errorAction.newProps.error = error.code;
break;
case "storage/canceled":
default:
errorAction.newProps.error = error.code;
break;
}
failures.push(file.name);
uploaderDispatch({ type: "file_update", data: errorAction });
if (isCompleted()) resolve(true);
},
// complete
() => {
uploaderDispatch({
type: "file_update",
data: {
fileName: file.name,
newProps: { loading: false },
},
});
// Upload completed successfully, now we can get the download URL
getDownloadURL(uploadTask.snapshot.ref).then(
(downloadURL: string) => {
// Store in the document if docRef provided
// if (docRef && docRef.update)docRef.update({ [fieldName]: newValue });
// Also call callback if it exists
// IMPORTANT: SideDrawer form may not update its local values after this
// function updates the doc, so you MUST update it manually
// using this callback
const obj = {
ref: uploadTask.snapshot.ref.fullPath,
downloadURL,
name: file.name,
type: file.type,
lastModifiedTS: file.lastModified,
};
uploads.push(obj);
if (isCompleted()) resolve(true);
}
);
}
);
})
).then(() => {
uploaderDispatch({ type: "reset" });
return { uploads, failures };
});
};
const deleteUpload = (fileValue: FileValue) => {
if (fileValue.ref) return deleteObject(ref(firebaseStorage, fileValue.ref));
else {
return true;
}
};
return { uploaderState, upload, uploaderDispatch, deleteUpload };
};
export default useFirebaseStorageUploader;