Skip to content

Commit

Permalink
Resize uploaded logo
Browse files Browse the repository at this point in the history
  • Loading branch information
sbutz committed Dec 30, 2023
1 parent c541834 commit 25977e5
Show file tree
Hide file tree
Showing 8 changed files with 39 additions and 24 deletions.
7 changes: 7 additions & 0 deletions client/src/components/ImageUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const maxFileSize = 1 * mb;

const minSideLength = 512;
const maxSideLength = 4096;
const uploadSideLength = 1024;
const aspect = 1;

const initialCrop : Crop = {
Expand Down Expand Up @@ -112,6 +113,12 @@ function ImageUpload({ label, value, onUpload: onChange }: InputBaseComponentPro

const onSave = async () => {
if (imageRef.current && crop) {
// limit upload resolution
const scaleX = imageRef.current.naturalWidth / imageRef.current.width;
const scaleY = imageRef.current.naturalHeight / imageRef.current.height;
crop.width = Math.min(crop.width, uploadSideLength / scaleX);
crop.height = Math.min(crop.height, uploadSideLength / scaleY);

const newImage = await cropImage(imageRef.current, crop as PixelCrop);
setImage(newImage);
onChange(newImage);
Expand Down
11 changes: 6 additions & 5 deletions client/src/store/ClubProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ export default function ClubProvider({ children }: ClubProviderProps) {
await updateDoc(doc(db, 'clubs', clubId), { name });
}, [clubId]);

const logoRef = clubId ? ref(storage, `${clubId}/logo.jpg`) : null;
const [logo, ,logoError] = useDownloadURL(logoRef);
const downloadRef = clubId ? ref(storage, `clubs/${clubId}/logo_512x512.jpeg`) : null;
const uploadRef = clubId ? ref(storage, `clubs/${clubId}/logo.jpeg`) : null;
const [logo, ,logoError] = useDownloadURL(downloadRef);
useEffect(() => { if (logoError && logoError.code !== 'storage/object-not-found') throw logoError; }, [logoError]);

const [localLogo, setLocalLogo] = useState<string>();
Expand All @@ -49,11 +50,11 @@ export default function ClubProvider({ children }: ClubProviderProps) {
useEffect(() => { if (errorUploadLogo) throw errorUploadLogo; }, [errorUploadLogo]);

const setLogo = useCallback(async (image: string) => {
if (!clubId || !logoRef) throw Error("Club's id not given. Cannot update it's logo.");
if (!clubId || !uploadRef) throw Error("Club's id not given. Cannot update it's logo.");
const blob = await (await fetch(image)).blob();
await uploadLogo(logoRef, blob, { contentType: 'image/png' });
await uploadLogo(uploadRef, blob, { contentType: 'image/jpeg' });
setLocalLogo(image); // browser won't refresh logo since url does not change, show local version
}, [clubId, logoRef, uploadLogo]);
}, [clubId, uploadRef, uploadLogo]);

const value = useMemo(() => ({
name: clubData?.name, setName, logo: localLogo, setLogo,
Expand Down
2 changes: 1 addition & 1 deletion client/src/util/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export async function cropImage(image: HTMLImageElement, completedCrop: PixelCro
);

const blob = await offscreen.convertToBlob({
type: 'image/png',
type: 'image/jpeg',
});
return encodeData(blob);
}
10 changes: 10 additions & 0 deletions extensions/storage-resize-images.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
DELETE_ORIGINAL_FILE=true
DO_BACKFILL=true
firebaseextensions.v1beta.function/location=europe-west3
FUNCTION_MEMORY=512
IMAGE_TYPE=jpeg
IMG_BUCKET=poolscore-1973.appspot.com
IMG_SIZES=256x256,512x512,1024x1024
IS_ANIMATED=false
MAKE_PUBLIC=false
RESIZED_IMAGES_PATH=./
3 changes: 3 additions & 0 deletions firebase.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,8 @@
},
"storage": {
"rules": "firebase/storage.rules"
},
"extensions": {
"storage-resize-images": "firebase/storage-resize-images@0.2.2"
}
}
25 changes: 10 additions & 15 deletions firebase/src/storage/clubs.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { readFileSync } from 'fs';
import { assertFails, assertSucceeds, initializeTestEnvironment, RulesTestEnvironment } from '@firebase/rules-unit-testing';
import {
assertFails, assertSucceeds, initializeTestEnvironment, RulesTestEnvironment,
} from '@firebase/rules-unit-testing';
import { uploadString, getBytes, StringFormat } from 'firebase/storage';

let testEnv: RulesTestEnvironment;
const clubId = 'bc73';
const logoPath = `clubs/${clubId}/logo.jpg`;
const logoPath = `clubs/${clubId}/logo.jpeg`;
const fooPath = `clubs/${clubId}/foo.png`;

beforeAll(async () => {
Expand All @@ -23,32 +25,25 @@ afterAll(async () => {
await testEnv.cleanup();
});

// deny other than logo.jpg
// deny large files
// deny wrong content type
// deny write for non-admin
// allow correct and as admin
// any club member should download

it('should only allow to update logo', async () => {
const adminStorage = testEnv.authenticatedContext('alice', { clubId, admin: true }).storage();
await assertFails(uploadString(adminStorage.ref(fooPath), '123', StringFormat.RAW, { contentType: "image/jpeg"}));
await assertFails(uploadString(adminStorage.ref(fooPath), '123', StringFormat.RAW, { contentType: 'image/jpeg' }));
});

it('should only allow jpeg images', async () => {
const adminStorage = testEnv.authenticatedContext('alice', { clubId, admin: true }).storage();
await assertFails(uploadString(adminStorage.ref(logoPath), '123', StringFormat.RAW, { contentType: "image/png"}));
await assertFails(uploadString(adminStorage.ref(logoPath), '123', StringFormat.RAW, { contentType: 'image/png' }));
});

it('should only allow images up to 512kb', async () => {
it('should only allow images up to 1Mb', async () => {
const adminStorage = testEnv.authenticatedContext('alice', { clubId, admin: true }).storage();
await assertFails(uploadString(adminStorage.ref(logoPath), '0'.repeat(512*1024+1), StringFormat.RAW, { contentType: "image/jpeg"}));
await assertFails(uploadString(adminStorage.ref(logoPath), '0'.repeat(1 * 1024 * 1024 + 1), StringFormat.RAW, { contentType: 'image/jpeg' }));
});

it('should allow create and update correct data', async () => {
const adminStorage = testEnv.authenticatedContext('alice', { clubId, admin: true }).storage();
await assertSucceeds(uploadString(adminStorage.ref(logoPath), '123', StringFormat.RAW, { contentType: "image/jpeg"}));
await assertSucceeds(uploadString(adminStorage.ref(logoPath), '1234', StringFormat.RAW, { contentType: "image/jpeg"}));
await assertSucceeds(uploadString(adminStorage.ref(logoPath), '123', StringFormat.RAW, { contentType: 'image/jpeg' }));
await assertSucceeds(uploadString(adminStorage.ref(logoPath), '1234', StringFormat.RAW, { contentType: 'image/jpeg' }));
});

it('should allow read for all club members', async () => {
Expand Down
4 changes: 2 additions & 2 deletions firebase/storage.rules
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ service firebase.storage {

match /clubs/{clubId}/{image} {
function canUpdate() {
return image in ['logo.jpg'].toSet()
&& request.resource.size <= 512*1024
return image in ['logo.jpeg'].toSet()
&& request.resource.size <= 1*1024*1024
&& request.resource.contentType in ['image/jpeg'].toSet();
}

Expand Down
1 change: 0 additions & 1 deletion functions/src/createClub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export async function createClub(): Promise<BeforeCreateResponse> {
export const deleteClub = onDocumentDeleted(
"clubs/{clubId}",
async (event) => {
// todo:
const snapshot = event.data;
if (!snapshot) return;

Expand Down

0 comments on commit 25977e5

Please sign in to comment.