Skip to content

Commit 37a64ed

Browse files
feat: Upload files to Azure blob storage account instead of cloudinary (#476)
1 parent ee856fd commit 37a64ed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+7886
-567
lines changed

.env.example

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,7 @@ SKIP_SETUP=false
3434
SHOW_REMIX_DEVTOOLS=false
3535
IMAGE_DIR="./data"
3636

37-
SENTRY_DSN="xxx"
37+
SENTRY_DSN="xxx"
38+
39+
AZURE_BLOB_KEY=xxx
40+
AZURE_BLOB_NAME=xxx

.github/workflows/e2e.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ jobs:
9393
PWA_PUBLIC_KEY: "xxx"
9494
PWA_PRIVATE_KEY: "xxx"
9595
IMAGE_DIR: "/data"
96+
AZURE_BLOB_KEY: "xxx"
97+
AZURE_BLOB_NAME: "xxx"
9698

9799
steps:
98100
- uses: actions/checkout@v6

app/.server/db/gataEvent.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,10 @@ export const insertCloudinaryImage = async (eventId: number, values: typeof clou
172172
await db.insert(cloudinaryImage).values(values);
173173
await db.insert(eventCloudinaryImages).values({ eventId, cloudId: values.cloudId });
174174
};
175+
export const insertAzureBlob = async (eventId: number, values: (typeof cloudinaryImage.$inferInsert)[]) => {
176+
await db.insert(cloudinaryImage).values(values);
177+
await db.insert(eventCloudinaryImages).values(values.map((file) => ({ eventId, cloudId: file.cloudId })));
178+
};
175179

176180
export const getEventCloudinaryImages = async (eventId: number) => {
177181
return await db
@@ -180,15 +184,16 @@ export const getEventCloudinaryImages = async (eventId: number) => {
180184
cloudUrl: cloudinaryImage.cloudUrl,
181185
width: cloudinaryImage.width,
182186
height: cloudinaryImage.height,
187+
type: cloudinaryImage.type,
183188
})
184189
.from(eventCloudinaryImages)
185190
.innerJoin(cloudinaryImage, eq(cloudinaryImage.cloudId, eventCloudinaryImages.cloudId))
186191
.orderBy(desc(sql`${eventCloudinaryImages}.rowid`))
187-
.where(eq(eventCloudinaryImages.eventId, eventId));
192+
.where(and(eq(eventCloudinaryImages.eventId, eventId), eq(cloudinaryImage.isDeleted, false)));
188193
};
189194

190195
export const deleteEventCloudinaryImage = async (cloudId: string) => {
191-
await db.delete(cloudinaryImage).where(eq(cloudinaryImage.cloudId, cloudId));
196+
await db.update(cloudinaryImage).set({ isDeleted: true }).where(eq(cloudinaryImage.cloudId, cloudId));
192197
};
193198

194199
export const getNumberOfImages = async (eventId: number) => {

app/.server/db/report.ts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { desc, eq, sql } from "drizzle-orm";
22

33
import { db } from "db/config.server";
4-
import { gataReport, reportFile } from "db/schema";
4+
import { gataReport, oldReportFiles, cloudinaryImage, reportFiles } from "db/schema";
55
import { ReportType } from "~/types/GataReport.type";
66
import type { ReportSchema } from "~/utils/formSchema";
77

88
import type { User } from "./user";
9-
import { deleteImage } from "../services/cloudinaryService";
109

1110
export const getReportsSimple = async (reportType: ReportType) => {
1211
return await db
@@ -90,14 +89,13 @@ export const updateReport = async (reportId: string, values: ReportSchema, logge
9089
};
9190

9291
export const deleteReport = async (reportId: string) => {
93-
const reportFiles = await db.select().from(reportFile).where(eq(reportFile.reportId, reportId));
92+
const reportFiles = await db.select().from(oldReportFiles).where(eq(oldReportFiles.reportId, reportId));
9493
await Promise.all(
9594
reportFiles.map(async (file) => {
9695
if (!file.cloudId) {
9796
throw new Error("No cloud id!");
9897
}
99-
await deleteImage(file.cloudId);
100-
await db.delete(reportFile).where(eq(reportFile.id, file.id));
98+
await db.delete(oldReportFiles).where(eq(oldReportFiles.id, file.id));
10199
})
102100
);
103101
await db.delete(gataReport).where(eq(gataReport.id, reportId));
@@ -113,3 +111,35 @@ export const updateReportContent = async (reportId: string, content: string, log
113111
})
114112
.where(eq(gataReport.id, reportId));
115113
};
114+
115+
export const getAllReportsWithOldFiles = async () => {
116+
const reports = await db
117+
.select({
118+
id: gataReport.id,
119+
content: gataReport.content,
120+
})
121+
.from(gataReport);
122+
123+
const reportsWithFiles = await Promise.all(
124+
reports.map(async (report) => {
125+
const files = await db.select().from(oldReportFiles).where(eq(oldReportFiles.reportId, report.id));
126+
return { ...report, oldFiles: files };
127+
})
128+
);
129+
130+
return reportsWithFiles.filter((report) => report.oldFiles.length > 0);
131+
};
132+
133+
export const batchInsertCloudImages = async (images: Array<typeof cloudinaryImage.$inferInsert>) => {
134+
if (images.length === 0) return;
135+
await db.insert(cloudinaryImage).values(images);
136+
};
137+
138+
export const batchInsertReportFiles = async (reportFileRecords: Array<{ reportId: string; fileId: string }>) => {
139+
if (reportFileRecords.length === 0) return;
140+
await db.insert(reportFiles).values(reportFileRecords);
141+
};
142+
143+
export const updateReportContentOnly = async (reportId: string, content: string) => {
144+
await db.update(gataReport).set({ content }).where(eq(gataReport.id, reportId));
145+
};

app/.server/db/reportFile.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { eq } from "drizzle-orm";
22

33
import { db } from "db/config.server";
4-
import { reportFile } from "db/schema";
4+
import { oldReportFiles, reportFiles, cloudinaryImage } from "db/schema";
55

6-
export const insertReportFile = async (values: Omit<typeof reportFile.$inferInsert, "id">) => {
7-
return await db.insert(reportFile).values(values).returning();
6+
export const insertReportFile = async (reportId: string, file: Omit<typeof cloudinaryImage.$inferInsert, "id">) => {
7+
await db.insert(cloudinaryImage).values(file).returning();
8+
await db.insert(reportFiles).values({ fileId: file.cloudId, reportId });
89
};
910

1011
export const getReportFile = async (fileId: string) => {
11-
const [result] = await db.select().from(reportFile).where(eq(reportFile.id, fileId));
12+
const [result] = await db.select().from(oldReportFiles).where(eq(oldReportFiles.id, fileId));
1213
return result;
1314
};

app/.server/services/cloudinaryService.ts

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,6 @@ cloudinary.v2.config({
99
api_secret: process.env.CLOUDINARY_API_SECRET,
1010
});
1111

12-
export const uploadImage = (data: string, folder: string) => {
13-
return new Promise<UploadApiResponse>((resolve, reject) => {
14-
void cloudinary.v2.uploader.upload(data, { folder }, (error, result) => {
15-
if (error) {
16-
console.error(error);
17-
reject(error as Error);
18-
}
19-
if (result) {
20-
resolve(result);
21-
}
22-
});
23-
});
24-
};
25-
2612
export const generateZip = (folderName: string, zipName: string) => {
2713
const zipUrl = cloudinary.v2.utils.download_zip_url({
2814
resource_type: "all",
@@ -33,18 +19,6 @@ export const generateZip = (folderName: string, zipName: string) => {
3319
return zipUrl;
3420
};
3521

36-
export const deleteImage = (publicId: string) => {
37-
return new Promise((resolve, reject) => {
38-
void cloudinary.v2.uploader.destroy(publicId, (error, result) => {
39-
if (error) {
40-
reject(error as Error);
41-
} else {
42-
resolve(result);
43-
}
44-
});
45-
});
46-
};
47-
4822
export function uploadImageToCloudinary(data: ArrayBuffer, folder: string) {
4923
const buffer = Buffer.from(data);
5024
const uploadPromise = new Promise<UploadApiResponse>((resolve, reject) => {

app/.server/services/localImageService.ts

Lines changed: 0 additions & 21 deletions
This file was deleted.

app/components/AvatarUser.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { Image } from "@unpic/react";
2-
31
import type { User } from "~/.server/db/user";
42
import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar";
53
import { cn } from "~/utils";
@@ -42,8 +40,7 @@ export const AvatarUserButton = ({ user, className }: Props) => {
4240

4341
<Dialog ref={dialogRef} className="backdrop:bg-black/80">
4442
<DialogCloseButton onClose={close} />
45-
<Image
46-
unstyled
43+
<img
4744
className={cn("max-h-screen object-contain")}
4845
src={user?.picture || ""}
4946
alt=""

app/components/CloudImage.tsx

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
import { Image } from "@unpic/react";
21
import { Video } from "lucide-react";
32

43
import type { CloudinaryImage } from "db/schema";
5-
import { getIsVideo } from "~/utils/cloudinaryUtils";
4+
import { getIsVideo } from "~/utils/file.utils";
65

76
type Props = {
87
cloudImage: CloudinaryImage;
98
};
109

1110
export const CloudImage = ({ cloudImage }: Props) => {
12-
if (getIsVideo(cloudImage.cloudUrl)) {
11+
if (getIsVideo(cloudImage)) {
1312
return (
1413
<div className="relative h-full object-cover">
1514
<video className="h-full rounded shadow" loop muted playsInline preload="metadata">
@@ -22,14 +21,6 @@ export const CloudImage = ({ cloudImage }: Props) => {
2221
);
2322
}
2423
return (
25-
<Image
26-
className="rounded shadow h-full object-cover"
27-
src={cloudImage.cloudUrl}
28-
alt=""
29-
height={300}
30-
unstyled
31-
background="auto"
32-
width={200}
33-
/>
24+
<img className="rounded shadow h-full object-cover" src={cloudImage.cloudUrl} alt="" height={300} width={200} />
3425
);
3526
};

app/components/CloudImageFullscreen.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
import { Image } from "@unpic/react";
21
import { ChevronLeft, ChevronRight, Loader2, X } from "lucide-react";
32
import { useEffect, useState } from "react";
43

54
import type { CloudinaryImage } from "db/schema";
65
import { Button } from "~/components/ui/button";
76
import { cn } from "~/utils";
8-
import { getIsVideo } from "~/utils/cloudinaryUtils";
97
import { useDialog } from "~/utils/dialogUtils";
8+
import { getIsVideo } from "~/utils/file.utils";
109

1110
import { Typography } from "./ui/typography";
1211

@@ -20,6 +19,7 @@ type Props = {
2019

2120
export const CloudImageFullscreen = ({
2221
cloudImage: { cloudUrl, width, height },
22+
cloudImage,
2323
onNext,
2424
onPrevious,
2525
onClose,
@@ -29,12 +29,12 @@ export const CloudImageFullscreen = ({
2929
const [isLoaded, setIsLoaded] = useState(false);
3030

3131
useEffect(() => {
32-
if (getIsVideo(cloudUrl)) {
32+
if (getIsVideo(cloudImage)) {
3333
setIsLoaded(true);
3434
} else {
3535
setIsLoaded(false);
3636
}
37-
}, [cloudUrl]);
37+
}, [cloudImage]);
3838

3939
return (
4040
<>
@@ -92,7 +92,7 @@ export const CloudImageFullscreen = ({
9292
>
9393
<Loader2 className="animate-spin size-12" />
9494
</Typography>
95-
{getIsVideo(cloudUrl) ? (
95+
{getIsVideo(cloudImage) ? (
9696
<video
9797
className={cn("max-h-screen object-contain max-w-full", isLoaded ? "opacity-100" : "opacity-30")}
9898
controls
@@ -105,13 +105,11 @@ export const CloudImageFullscreen = ({
105105
<track default kind="captions" />
106106
</video>
107107
) : (
108-
<Image
108+
<img
109109
loading="eager"
110-
unstyled
111110
className={cn("max-h-screen object-contain", isLoaded ? "opacity-100" : "opacity-30")}
112111
fetchPriority="high"
113112
src={cloudUrl}
114-
background="auto"
115113
alt=""
116114
width={width}
117115
height={height}

0 commit comments

Comments
 (0)