Skip to content

Commit

Permalink
Merge pull request #115 from lyve-app/feat/upload-images
Browse files Browse the repository at this point in the history
feat: added img upload for create stream and update user closes #85
  • Loading branch information
Louis3797 authored Jun 13, 2024
2 parents fe9c773 + 0708b21 commit f026349
Show file tree
Hide file tree
Showing 13 changed files with 152 additions and 28 deletions.
6 changes: 5 additions & 1 deletion apps/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,8 @@ CORS_ORIGIN=*

# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/lyve_db?schema=public"
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/lyve_db?schema=public"

AZURE_STORAGE_ACCOUNT_NAME=
AZURE_STORAGE_ACCOUNT_KEY=
AZURE_STORAGE_CONTAINER_NAME=
5 changes: 4 additions & 1 deletion apps/api/.env.local
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ CORS_ORIGIN=*

# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/lyve_db?schema=public"
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/lyve_db?schema=public"
AZURE_STORAGE_ACCOUNT_NAME=
AZURE_STORAGE_ACCOUNT_KEY=
AZURE_STORAGE_CONTAINER_NAME=
3 changes: 3 additions & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
},
"private": true,
"dependencies": {
"@azure/storage-blob": "^12.23.0",
"@prisma/client": "^5.13.0",
"amqplib": "^0.10.4",
"base-64": "^1.0.0",
Expand All @@ -31,6 +32,7 @@
"joi": "^17.13.0",
"jwt-decode": "^4.0.0",
"mediasoup": "^3.14.7",
"multer": "^1.4.5-lts.1",
"socket.io": "^4.7.5",
"tslib": "^2.3.0",
"utility-types": "^3.11.0",
Expand All @@ -46,6 +48,7 @@
"@types/dotenv": "^8.2.0",
"@types/express": "~4.17.13",
"@types/jest": "^29.4.0",
"@types/multer": "^1.4.11",
"@types/node": "~18.16.9",
"@types/supertest": "^6.0.2",
"cross-env": "^7.0.3",
Expand Down
15 changes: 15 additions & 0 deletions apps/api/src/config/azure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {
BlobServiceClient,
StorageSharedKeyCredential
} from "@azure/storage-blob";
import config from "./config";

const sharedKeyCredential = new StorageSharedKeyCredential(
config.azure.storage.account.name,
config.azure.storage.account.key
);

export const blobServiceClient = new BlobServiceClient(
`https://${config.azure.storage.account.name}.blob.core.windows.net`,
sharedKeyCredential
);
16 changes: 15 additions & 1 deletion apps/api/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ const envSchema = Joi.object().keys({
PORT: Joi.number().port().required().default(4040),
HOST: Joi.string().required(),
CORS_ORIGIN: Joi.string().required().default("*"),
RABBITMQ_URL: Joi.string().required()
RABBITMQ_URL: Joi.string().required(),
AZURE_STORAGE_ACCOUNT_NAME: Joi.string().required(),
AZURE_STORAGE_ACCOUNT_KEY: Joi.string().required(),
AZURE_STORAGE_CONTAINER_NAME: Joi.string().required()
});

const { value: validatedEnv, error } = envSchema
Expand Down Expand Up @@ -42,6 +45,17 @@ const config = {
media_server_queue: "media_server_queue"
},
retryInterval: 5000
},
azure: {
storage: {
account: {
name: validatedEnv.AZURE_STORAGE_ACCOUNT_NAME,
key: validatedEnv.AZURE_STORAGE_ACCOUNT_KEY
},
container: {
name: validatedEnv.AZURE_STORAGE_CONTAINER_NAME
}
}
}
} as const;

Expand Down
1 change: 0 additions & 1 deletion apps/api/src/controller/search.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,6 @@ export const search = async (
hasNext = next.length > 0;
}

// Todo add real subscribed value
const responseData: Array<
Pick<
User,
Expand Down
44 changes: 32 additions & 12 deletions apps/api/src/controller/stream.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@ import type { Request, Response } from "express";
import httpStatus from "http-status";
import prismaClient from "../config/prisma";
import { Prisma, Stream, User } from "@prisma/client";
import {
CreateStreamCredentials,
TypedRequest,
TypedResponse
} from "../types/types";
import { TypedRequest, TypedResponse } from "../types/types";
import { createErrorObject } from "../utils/createErrorObject";
import { getNotificationsMessage } from "../utils/notificationsMessages";
import { uploadFileToBlob } from "../service/blob.service";
import path from "path";

export const getStreamInfo = async (
req: Request<{ id: string }>,
Expand Down Expand Up @@ -103,7 +101,7 @@ export const getStreamInfo = async (
};

export const createStream = async (
req: TypedRequest<CreateStreamCredentials>,
req: TypedRequest<{ genre: string }>,
res: Response<
TypedResponse<{
stream: Stream & {
Expand All @@ -122,17 +120,14 @@ export const createStream = async (
>
) => {
const { user } = req;
const { previewImgUrl, genre } = req.body;
const { genre } = req.body;

if (!user || !previewImgUrl || !genre) {
if (!user || !genre) {
return res.status(httpStatus.BAD_REQUEST).json({
success: false,
data: null,
error: [
...createErrorObject(
httpStatus.BAD_REQUEST,
"streamerId, image and genre must be defined"
)
...createErrorObject(httpStatus.BAD_REQUEST, " genre must be defined")
]
});
}
Expand All @@ -157,6 +152,31 @@ export const createStream = async (
});
}

let previewImgUrl = "";

if (req.file) {
try {
const buffer = req.file.buffer;
const fileExtension = path.extname(req.file.originalname);
const blobName = `${crypto.randomUUID()}${fileExtension}`;

const uploadedImage = await uploadFileToBlob(buffer, blobName);
previewImgUrl = uploadedImage.url;
} catch (error) {
console.log(error);
return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
success: false,
data: null,
error: [
...createErrorObject(
httpStatus.INTERNAL_SERVER_ERROR,
"Error uploading file."
)
]
});
}
}

const stream = await prismaClient.stream.create({
data: {
streamerId: user.id,
Expand Down
50 changes: 45 additions & 5 deletions apps/api/src/controller/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
} from "@prisma/client";
import { createErrorObject } from "../utils/createErrorObject";
import { getNotificationsMessage } from "../utils/notificationsMessages";
import path from "path";
import { uploadFileToBlob } from "../service/blob.service";

export const getUserInfo = async (
req: Request<{ id: string }>,
Expand Down Expand Up @@ -600,7 +602,7 @@ export const updateUser = async (
req: Request<
{ id: string },
Record<string, unknown>,
{ dispname?: string; avatar_url?: string; bio?: string }
{ dispname?: string; bio?: string }
>,
res: Response<
TypedResponse<{
Expand Down Expand Up @@ -641,14 +643,52 @@ export const updateUser = async (
});
}

const { dispname, avatar_url, bio } = req.body;
const { dispname, bio } = req.body;

if (!dispname && !bio && !req.file) {
return res.status(httpStatus.BAD_REQUEST).json({
success: false,
data: null,
error: [
...createErrorObject(
httpStatus.BAD_REQUEST,
"dispname, bio and file are all not defined. Define at least one"
)
]
});
}

let avatar_url = "";

if (req.file) {
try {
const buffer = req.file.buffer;
const fileExtension = path.extname(req.file.originalname);
const blobName = `${crypto.randomUUID()}${fileExtension}`;

const uploadedImage = await uploadFileToBlob(buffer, blobName);
avatar_url = uploadedImage.url;
} catch (error) {
console.log(error);
return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
success: false,
data: null,
error: [
...createErrorObject(
httpStatus.INTERNAL_SERVER_ERROR,
"Error uploading file."
)
]
});
}
}

const updatedUser = await prismaClient.user.update({
where: { id },
data: {
dispname: dispname ?? checkUser.dispname,
avatar_url: avatar_url ?? checkUser.avatar_url,
bio: bio ?? checkUser.bio
...(dispname && { dispname }),
...(bio && { bio }),
...(avatar_url && { avatar_url })
}
});

Expand Down
5 changes: 5 additions & 0 deletions apps/api/src/middleware/upload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import multer from "multer";

const storage = multer.memoryStorage(); // Use memory storage

export const upload = multer({ storage });
7 changes: 6 additions & 1 deletion apps/api/src/routes/stream.route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { Router } from "express";
import { streamController } from "../controller";
import { upload } from "../middleware/upload";

const streamRouter = Router();

streamRouter.post("/create", streamController.createStream);
streamRouter.post(
"/create",
upload.single("image"),
streamController.createStream
);

streamRouter.get("/recommended", streamController.getRecommended);

Expand Down
7 changes: 6 additions & 1 deletion apps/api/src/routes/user.route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Router } from "express";
import { userController } from "../controller";
import { upload } from "../middleware/upload";

const userRouter = Router();

Expand All @@ -19,7 +20,11 @@ userRouter.get("/:id/feed", userController.getFeed);

userRouter.get("/:id/notifications", userController.getNotifications);

userRouter.put("/:id/update", userController.updateUser);
userRouter.put(
"/:id/update",
upload.single("image"),
userController.updateUser
);

userRouter.get(
"/:id/statistics/most-streamed-genre",
Expand Down
16 changes: 16 additions & 0 deletions apps/api/src/service/blob.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import config from "../config/config";
import { blobServiceClient } from "../config/azure";

export const uploadFileToBlob = async (
buffer: Buffer,
blobName: string
): Promise<{ url: string }> => {
const containerClient = blobServiceClient.getContainerClient(
config.azure.storage.container.name
);
const blockBlobClient = containerClient.getBlockBlobClient(blobName);
await blockBlobClient.uploadData(buffer);

// Return the URL of the uploaded file
return { url: blockBlobClient.url };
};
5 changes: 0 additions & 5 deletions apps/api/src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,6 @@ export type CreateUserCredentials = {
email: string;
};

export type CreateStreamCredentials = {
previewImgUrl: string;
genre: string;
};

export type ChatMessage = {
id: string;
msg?: string;
Expand Down

0 comments on commit f026349

Please sign in to comment.