Skip to content

Commit

Permalink
feat: Added s3 image file upload
Browse files Browse the repository at this point in the history
  • Loading branch information
lewisblackburn committed Sep 6, 2024
1 parent 6ac3436 commit 88f91ab
Show file tree
Hide file tree
Showing 11 changed files with 6,606 additions and 4,669 deletions.
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,9 @@ GITHUB_TOKEN="MOCK_GITHUB_TOKEN"
# default to allow indexing for seo safety
ALLOW_INDEXING="true"

STORAGE_ACCESS_KEY="<access key ID>"
STORAGE_SECRET="<secret key>"
STORAGE_BUCKET="<s3 bucket name>"
STORAGE_REGION="<s3 bucket region>"

TMDB_ACCESS_TOKEN="MOCK_TMDB_ACCESS_TOKEN"
2 changes: 1 addition & 1 deletion app/routes/dashboard+/admin+/import+/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import { type TMDBSearchResponse } from '#app/types/tmdb.js'
import { useDebounce } from '#app/utils/misc.js'
import { requireUserWithRole } from '#app/utils/permissions.server.js'
import { getSearchParams } from '#app/utils/request-helper.js'
import { tmdb } from '#app/utils/services/import.service.js'
import { TMDBFilmImporter } from '#app/utils/services/tmdb/film.js'
import { tmdb } from '#app/utils/services/tmdb/index.js'
import { createToastHeaders } from '#app/utils/toast.server.js'
import { columns } from './table/import/columns'
import { ImportFilmTable } from './table/import/data-table'
Expand Down
5 changes: 5 additions & 0 deletions app/utils/env.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ const schema = z.object({
GITHUB_TOKEN: z.string().default('MOCK_GITHUB_TOKEN'),
ALLOW_INDEXING: z.enum(['true', 'false']).optional(),

STORAGE_ACCESS_KEY: z.string(),
STORAGE_SECRET: z.string(),
STORAGE_REGION: z.string(),
STORAGE_BUCKET: z.string(),

TMDB_ACCESS_TOKEN: z.string(),
})

Expand Down
2 changes: 2 additions & 0 deletions app/utils/extensions/film.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,5 @@ export const filmFormatting = Prisma.defineExtension((client) => {
},
})
})

// TODO: When deleting a film, delete the entire film folder in AWS S3 (The same goes for the rest of the models)
120 changes: 120 additions & 0 deletions app/utils/s3.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import path from 'path'
import { PassThrough } from 'stream'
import { S3 } from '@aws-sdk/client-s3'
import { Upload } from '@aws-sdk/lib-storage'
import {
type UploadHandler,
writeAsyncIterableToWritable,
} from '@remix-run/node'
import { v4 as uuidv4 } from 'uuid'

const { STORAGE_ACCESS_KEY, STORAGE_SECRET, STORAGE_REGION, STORAGE_BUCKET } =
process.env

interface UploadStreamOptions {
Key: string
}

const createS3Client = () =>
new S3({
credentials: {
accessKeyId: STORAGE_ACCESS_KEY!,
secretAccessKey: STORAGE_SECRET!,
},
region: STORAGE_REGION!,
})

const uploadStream = ({ Key }: UploadStreamOptions) => {
const s3 = createS3Client()
const pass = new PassThrough()

return {
writeStream: pass,
promise: new Upload({
client: s3,
params: { Bucket: STORAGE_BUCKET!, Key, Body: pass },
}).done(),
}
}

export async function uploadStreamToS3(
data: AsyncIterable<Uint8Array>,
filename: string,
folder?: string,
) {
const key = folder ? `${folder}/${filename}` : filename
const stream = uploadStream({ Key: key })
await writeAsyncIterableToWritable(data, stream.writeStream)
const file = await stream.promise
return file.Location
}

export const createUploadHandler =
(folder: string = 'images'): UploadHandler =>
async ({ data, filename, name }) => {
if (name !== 'image') return undefined

const fileType = path.extname(filename!).toLowerCase()
const newFilename = `${uuidv4()}${fileType}`
return await uploadStreamToS3(data, newFilename, folder)
}

const deleteFromS3 = async (keys: string[]) => {
const s3 = createS3Client()

await Promise.all(
keys.map(async (key) => {
try {
await s3.deleteObject({ Bucket: STORAGE_BUCKET!, Key: key })
} catch (error) {
console.error(`Error deleting ${key} from S3:`, error)
throw error
}
}),
)
}

const deleteFolderFromS3 = async (prefix: string = '') => {
const s3 = createS3Client()

try {
const listedObjects = await s3.listObjectsV2({
Bucket: STORAGE_BUCKET!,
Prefix: prefix,
})

if (listedObjects.Contents?.length) {
const keys = listedObjects.Contents.map((object) => object.Key!)
while (keys.length) {
const batch = keys.splice(0, 1000)
await s3.deleteObjects({
Bucket: STORAGE_BUCKET!,
Delete: { Objects: batch.map((key) => ({ Key: key })) },
})
}
}
} catch (error) {
console.error(
`Error deleting objects with prefix ${prefix} from S3:`,
error,
)
throw error
}
}

export const s3DeleteHandler = async (
filenames: string[],
isFolder: boolean = false,
) => {
try {
if (isFolder && filenames.length > 0) {
const prefix = filenames[0]
await deleteFolderFromS3(prefix)
return `Successfully deleted folder with prefix ${prefix} from S3.`
}
await deleteFromS3(filenames)
return `Successfully deleted ${filenames.length} files from S3.`
} catch (error) {
return `Error deleting files or folder from S3: ${error}`
}
}
62 changes: 0 additions & 62 deletions app/utils/services/import.service.ts

This file was deleted.

94 changes: 77 additions & 17 deletions app/utils/services/tmdb/film.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
import { type Film, type Prisma } from '@prisma/client'
import { type TMDBFilm } from '#app/types/tmdb.js'
import { prisma } from '#app/utils/db.server.js'
import { TMDBImporter } from './importer'
import { TMDB } from '.'

type FilmData =
| (Prisma.Without<Prisma.FilmCreateInput, Prisma.FilmUncheckedCreateInput> &
Prisma.FilmUncheckedCreateInput)
| (Prisma.Without<Prisma.FilmUncheckedCreateInput, Prisma.FilmCreateInput> &
Prisma.FilmCreateInput)

export class TMDBFilmImporter extends TMDBImporter<Film> {
async addPosterImage<Film>(): Promise<Film | false> {
throw new Error('Method not implemented.')
}

export class TMDBFilmImporter extends TMDB {
async isExistingEntity(filmId: string): Promise<Film | null> {
return prisma.film.findUnique({
where: {
tmdbID: filmId,
tmdbID: filmId.toString(),
},
})
}
Expand All @@ -29,10 +25,10 @@ export class TMDBFilmImporter extends TMDBImporter<Film> {
}

async import(filmId: string): Promise<Film | false> {
const existingFilm = await this.isExistingEntity(filmId.toString())
const existingFilm = await this.isExistingEntity(filmId)
if (existingFilm) return existingFilm

const film = await this.tmdb.getEntity<TMDBFilm>(
const film = await this.getEntity<TMDBFilm>(
'movie',
filmId,
'keywords,credits,videos,images',
Expand Down Expand Up @@ -104,16 +100,80 @@ export class TMDBFilmImporter extends TMDBImporter<Film> {
},
})

if (film.poster_path) {
// const posterPath = await this.tmdb.getPosterImage(film.poster_path)
// Save the posterPath if needed
}
await this.uploadAndSaveProfileImage(film.credits.cast)
await this.uploadAndSaveProfileImage(film.credits.crew)
if (film.poster_path)
await this.uploadAndSaveFilmImage(
film.poster_path,
'poster',
importedFilm.id,
)
if (film.backdrop_path)
await this.uploadAndSaveFilmImage(
film.backdrop_path,
'backdrop',
importedFilm.id,
)

if (film.backdrop_path) {
// const backdropPath = await this.tmdb.getBackdropImage(film.backdrop_path)
// Save the backdropPath if needed
return importedFilm
}

private async uploadAndSaveProfileImage(
people: TMDBFilm['credits']['cast'] | TMDBFilm['credits']['crew'],
) {
for (const person of people) {
try {
const profilePath = await this.uploadImage(
this.getProfileImage(person.profile_path ?? ''),
`person/${person.id}/profile`,
person.profile_path?.replace('/', '') ?? '',
)
await prisma.person.update({
where: { tmdbID: person.id.toString() },
data: {
tmdbID: person.id.toString(),
name: person.name,
image: profilePath || '',
photos: {
create: {
url: profilePath || '',
filename: person.profile_path?.replace('/', '') ?? '',
primary: true,
},
},
},
})
} catch {}
}
}

return importedFilm
private async uploadAndSaveFilmImage(
path: string,
type: 'poster' | 'backdrop',
filmId: string,
) {
try {
const imagePath = await this.uploadImage(
type === 'poster'
? this.getPosterImage(path)
: this.getBackdropImage(path),
`film/${filmId}/${type}`,
path.replace('/', ''),
)
await prisma.film.update({
where: { id: filmId },
data: {
[type]: imagePath || '',
photos: {
create: {
url: imagePath || '',
type,
filename: path.replace('/', ''),
primary: true,
},
},
},
})
} catch {}
}
}
13 changes: 0 additions & 13 deletions app/utils/services/tmdb/importer.ts

This file was deleted.

Loading

0 comments on commit 88f91ab

Please sign in to comment.