Skip to content

Commit

Permalink
Unify Logic in Image APIs (#647)
Browse files Browse the repository at this point in the history
* Implement coffee chat image API

* Run formatter

* Unify logic in image apis

* Unify api.ts,, rename functions, include file extension in path

* Fix floating value

* Revert adding file extensionn to pathname

* Fix profile image pathname

* Fix prefix placement + syntax

* Remove unused functions + fix nit

* Run formatter

---------

Co-authored-by: Andrew Chen <andrew032012@gmail.com>
  • Loading branch information
patriciaahuang and andrew032011 authored Oct 5, 2024
1 parent 67b0a19 commit f18e6d4
Show file tree
Hide file tree
Showing 10 changed files with 74 additions and 223 deletions.
73 changes: 40 additions & 33 deletions backend/src/API/imageAPI.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,41 @@
import { bucket } from '../firebase';
import { getNetIDFromEmail, filterImagesResponse } from '../utils/memberUtil';
import { filterImagesResponse } from '../utils/memberUtil';
import { NotFoundError } from '../utils/errors';

/**
* Sets image for member
* @param name - the name of the image
* @returns a Promise to the signed URL to the image file
*/
export const getWriteSignedURL = async (name: string): Promise<string> => {
const file = bucket.file(`${name}.jpg`);
const signedURL = await file.getSignedUrl({
action: 'write',
version: 'v4',
expires: Date.now() + 15 * 60000 // 15 min
});
return signedURL[0];
};

/**
* Gets image for member
* @param name - the name of the image
* @throws NotFoundError if the requested image does not exist
* @returns a Promise to the signed URL to the image file
*/
export const getReadSignedURL = async (name: string): Promise<string> => {
const file = bucket.file(`${name}.jpg`);
const fileExists = await file.exists().then((result) => result[0]);
if (!fileExists) {
throw new NotFoundError(`The requested image (${name}) does not exist`);
}
const signedURL = await file.getSignedUrl({
action: 'read',
expires: Date.now() + 15 * 60000
});
return signedURL[0];
};

/**
* Gets all profile images for members
* @returns - an array of ProfileImage objects which includes file name and URL
Expand All @@ -25,37 +59,10 @@ export const allMemberImages = async (): Promise<readonly ProfileImage[]> => {
};

/**
* Sets member image
* @param user - the member whose image will be set
* @returns - a Promise that represents the signedURL
*/
export const setMemberImage = async (user: IdolMember): Promise<string> => {
const netId: string = getNetIDFromEmail(user.email);
const file = bucket.file(`images/${netId}.jpg`);
const signedURL = await file.getSignedUrl({
action: 'write',
version: 'v4',
expires: Date.now() + 15 * 60000 // 15 min
});
return signedURL[0];
};

/**
* Gets member image
* @param user - the requested member
* @returns - a Promise that represents signedURL which can be used to get the image file
* @throws NotFoundError if the requested image does not exist
* Deletes image for member
* @param name - the name of the image
*/
export const getMemberImage = async (user: IdolMember): Promise<string> => {
const netId: string = getNetIDFromEmail(user.email);
const file = bucket.file(`images/${netId}.jpg`);
const fileExists = await file.exists().then((result) => result[0]);
if (!fileExists) {
throw new NotFoundError(`The requested image (${netId}.jpg) does not exist`);
}
const signedUrl = await file.getSignedUrl({
action: 'read',
expires: Date.now() + 15 * 60000
});
return signedUrl[0];
export const deleteImage = async (name: string): Promise<void> => {
const imageFile = bucket.file(`${name}.jpg`);
await imageFile.delete();
};
24 changes: 6 additions & 18 deletions backend/src/API/memberAPI.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import MembersDao from '../dao/MembersDao';
import PermissionsManager from '../utils/permissionsManager';
import { BadRequestError, PermissionError } from '../utils/errors';
import { bucket } from '../firebase';
import { getNetIDFromEmail, computeMembersDiff } from '../utils/memberUtil';
import { deleteImage } from './imageAPI';

const membersDao = new MembersDao();

Expand Down Expand Up @@ -96,24 +96,12 @@ export const deleteMember = async (email: string, user: IdolMember): Promise<voi
if (!email || email === '') {
throw new BadRequestError("Couldn't delete member with undefined email!");
}
await membersDao.deleteMember(email).then(() =>
deleteImage(email).catch(() => {
await membersDao.deleteMember(email).then(() => {
const netId: string = getNetIDFromEmail(email);
deleteImage(`images/${netId}`).catch(() => {
/* Ignore the error since the user might not have a profile picture. */
})
);
};

/**
* Deletes the profile picture of an IDOL member given their email.
* @param email - the email of the member profile picture to delete.
*/
export const deleteImage = async (email: string): Promise<void> => {
// Create a reference to the file to delete
const netId: string = getNetIDFromEmail(email);
const imageFile = bucket.file(`images/${netId}.jpg`);

// Delete the file
await imageFile.delete();
});
});
};

/**
Expand Down
97 changes: 0 additions & 97 deletions backend/src/API/teamEventsImageAPI.ts

This file was deleted.

39 changes: 11 additions & 28 deletions backend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import {
reviewUserInformationChange,
generateMemberArchive
} from './API/memberAPI';
import { getMemberImage, setMemberImage, allMemberImages } from './API/imageAPI';
import { allTeams, setTeam, deleteTeam } from './API/teamAPI';
import {
getAllShoutouts,
Expand Down Expand Up @@ -73,11 +72,6 @@ import {
updateCandidateDeciderInstance,
getCandidateDeciderReviews
} from './API/candidateDeciderAPI';
import {
deleteEventProofImage,
getEventProofImage,
setEventProofImage
} from './API/teamEventsImageAPI';
import {
getAllDevPortfolios,
createNewDevPortfolio,
Expand All @@ -91,6 +85,7 @@ import {
regradeSubmissions,
updateSubmissions
} from './API/devPortfolioAPI';
import { getWriteSignedURL, getReadSignedURL, deleteImage } from './API/imageAPI';
import DPSubmissionRequestLogDao from './dao/DPSubmissionRequestLogDao';
import AdminsDao from './dao/AdminsDao';
import { sendMail } from './API/mailAPI';
Expand Down Expand Up @@ -253,16 +248,17 @@ loginCheckedPost('/team', async (req, user) => ({
}));

// Images
loginCheckedGet('/member-image/:email', async (_, user) => ({
url: await getMemberImage(user)
loginCheckedGet('/image/:name(*)', async (req) => ({
url: await getReadSignedURL(req.params.name)
}));
// TODO: Modify this endpoint to /member-image/* to be more RESTful
loginCheckedGet('/member-image-signedURL', async (_, user) => ({
url: await setMemberImage(user)

loginCheckedGet('/image-signed-url/:name(*)', async (req) => ({
url: await getWriteSignedURL(req.params.name)
}));
router.get('/member-image', async (_, res) => {
const images = await allMemberImages();
res.status(200).json({ images });

loginCheckedDelete('/image/:name(*)', async (req) => {
await deleteImage(req.params.name);
return {};
});

// Shoutouts
Expand All @@ -288,6 +284,7 @@ loginCheckedDelete('/shoutout/:uuid', async (req, user) => {
return {};
});

// Coffee Chats
loginCheckedGet('/coffee-chat', async () => ({
coffeeChats: await getAllCoffeeChats()
}));
Expand Down Expand Up @@ -381,20 +378,6 @@ loginCheckedPost('/team-event-reminder', async (req, user) => ({
info: await notifyMember(req, req.query.end_of_semester_reminder !== undefined, req.body, user)
}));

// Team Events Proof Image
loginCheckedGet('/event-proof-image/:name(*)', async (req, user) => ({
url: await getEventProofImage(req.params.name, user)
}));

// TODO: Modify this endpoint to /event-proof-image/* to be more RESTful
loginCheckedGet('/event-proof-image-signed-url/:name(*)', async (req, user) => ({
url: await setEventProofImage(req.params.name, user)
}));
loginCheckedDelete('/event-proof-image/:name(*)', async (req, user) => {
await deleteEventProofImage(req.params.name, user);
return {};
});

// Candidate Decider
loginCheckedGet('/candidate-decider', async (_, user) => ({
instances: await getAllCandidateDeciderInstances(user)
Expand Down
2 changes: 1 addition & 1 deletion common-types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ interface TeamEvent extends TeamEventInfo {
readonly requests: TeamEventAttendance[];
}

interface EventProofImage {
interface Image {
readonly url: string;
readonly fileName: string;
}
Expand Down
48 changes: 9 additions & 39 deletions frontend/src/API/ImagesAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@ import APIWrapper from './APIWrapper';
import HeadshotPlaceholder from '../static/images/headshot-placeholder.png';

export default class ImagesAPI {
// member images
public static getMemberImage(email: string): Promise<string> {
const responseProm = APIWrapper.get(`${backendURL}/member-image/${email}`).then(
(res) => res.data
);
public static getImage(name: string): Promise<string> {
const responseProm = APIWrapper.get(`${backendURL}/image/${name}`).then((res) => res.data);

return responseProm.then((val) => {
if (val.error) {
Expand All @@ -17,48 +14,21 @@ export default class ImagesAPI {
});
}

private static getSignedURL(): Promise<string> {
const responseProm = APIWrapper.get(`${backendURL}/member-image-signedURL`).then(
(res) => res.data
);
return responseProm.then((val) => val.url);
}

public static uploadMemberImage(body: Blob): Promise<void> {
return this.getSignedURL().then((url) => {
const headers = { 'content-type': 'image/jpeg' };
APIWrapper.put(url, body, headers).then((res) => res.data);
});
}

// Event proof images
public static getEventProofImage(name: string): Promise<string> {
const responseProm = APIWrapper.get(`${backendURL}/event-proof-image/${name}`).then(
(res) => res.data
);
return responseProm.then((val) => {
if (val.error) {
return HeadshotPlaceholder.src;
}
return val.url;
});
}

private static getEventProofImageSignedURL(name: string): Promise<string> {
const responseProm = APIWrapper.get(`${backendURL}/event-proof-image-signed-url/${name}`).then(
private static getSignedURL(name: string): Promise<string> {
const responseProm = APIWrapper.get(`${backendURL}/image-signed-url/${name}`).then(
(res) => res.data
);
return responseProm.then((val) => val.url);
}

public static uploadEventProofImage(body: Blob, name: string): Promise<void> {
return this.getEventProofImageSignedURL(name).then((url) => {
const headers = { 'content-type': 'image/jpeg' };
public static uploadImage(body: Blob, name: string): Promise<void> {
return this.getSignedURL(name).then((url) => {
const headers = { 'content-type': body.type };
APIWrapper.put(url, body, headers).then((res) => res.data);
});
}

public static async deleteEventProofImage(name: string): Promise<void> {
await APIWrapper.delete(`${backendURL}/event-proof-image/${name}`);
public static async deleteImage(name: string): Promise<void> {
await APIWrapper.delete(`${backendURL}/image/${name}`);
}
}
Loading

0 comments on commit f18e6d4

Please sign in to comment.