diff --git a/Sources/swiftarr/Controllers/AdminController.swift b/Sources/swiftarr/Controllers/AdminController.swift index 02ad13dd4..b5c46b89e 100644 --- a/Sources/swiftarr/Controllers/AdminController.swift +++ b/Sources/swiftarr/Controllers/AdminController.swift @@ -939,7 +939,7 @@ struct AdminController: APIRouteCollection { var imageImportError: String? do { if let userImage = userToImport.userImage { - copiedUserImage = try await copyImage(userImage, verifyOnly: verifyOnly) + copiedUserImage = try await copyImage(userImage, verifyOnly: verifyOnly, on: req) } } catch { @@ -1071,7 +1071,7 @@ struct AdminController: APIRouteCollection { var copiedUserImage: String? do { if let image = performerData.photo.filename { - copiedUserImage = try await copyImage(image, verifyOnly: verifyOnly) + copiedUserImage = try await copyImage(image, verifyOnly: verifyOnly, on: req) } } catch { @@ -1132,7 +1132,7 @@ struct AdminController: APIRouteCollection { } // Copy an image from the uploaded data bundle to the expected location on the filesystem. - func copyImage(_ image: String, verifyOnly: Bool) async throws -> String { + func copyImage(_ image: String, verifyOnly: Bool, on req: Request) async throws -> String { let archiveSource = try uploadUserDirPath().appendingPathComponent("Twitarr_userfile/userImages", isDirectory: true) .appendingPathComponent(image) let serverImageDestDir = Settings.shared.userImagesRootPath.appendingPathComponent(ImageSizeGroup.full.rawValue) @@ -1149,6 +1149,9 @@ struct AdminController: APIRouteCollection { } try FileManager.default.copyItem(at: archiveSource, to: serverImageDest) } + if !verifyOnly { + try await regenerateThumbnail(for: serverImageDest, on: req) + } return image } diff --git a/Sources/swiftarr/Protocols/ImageHandler.swift b/Sources/swiftarr/Protocols/ImageHandler.swift index 6c9ebd00d..e364e876e 100644 --- a/Sources/swiftarr/Protocols/ImageHandler.swift +++ b/Sources/swiftarr/Protocols/ImageHandler.swift @@ -213,7 +213,47 @@ extension APIRouteCollection { return fullPath.lastPathComponent }.get() } - + + // Generate a thumbnail for the given image (by full path). Currently used in `copyImage` + // as part of the bulk user import process. Could stand to be deduped with `processImage` + // above some day since most of the code came from there. + func regenerateThumbnail(for imageSource: URL, on req: Request) async throws { + let imageName = imageSource.lastPathComponent + return try await req.application.threadPool.runIfActive(eventLoop: req.eventLoop) { + let data = try Data(contentsOf: imageSource) + + let imageTypes: [ImportableFormat] = [.jpg, .png, .gif, .webp, .tiff, .bmp, .wbmp] + var foundType: ImportableFormat? = nil + var foundImage: GDImage? + for type in imageTypes { + foundImage = try? GDImage(data: data, as: type) + if foundImage != nil{ + foundType = type + break + } + } + guard let image = foundImage, let imageType = foundType else { + throw GDError.invalidImage(reason: "No matching raster formatter for given image found") + } + + let destinationDir = Settings.shared.userImagesRootPath + .appendingPathComponent(ImageSizeGroup.thumbnail.rawValue) + .appendingPathComponent(String(imageName.prefix(2))) + // Testing this requires wiping out the thumbnail directory. + if (!FileManager.default.fileExists(atPath: destinationDir.path)) { + try FileManager.default.createDirectory(at: destinationDir, withIntermediateDirectories: true) + } + let thumbPath = destinationDir.appendingPathComponent(imageName) + + guard let thumbnail = image.resizedTo(height: Settings.shared.imageThumbnailSize) else { + throw Abort(.internalServerError, reason: "Error generating thumbnail image") + } + + let thumbnailData = try thumbnail.export(as: ExportableFormat(imageType)) + try thumbnailData.write(to: thumbPath) + }.get() + } + /// Archives an image that is no longer needed other than for accountability tracking, by /// removing the full-sized image and moving the thumbnail into the `archive/` subdirectory /// of the provided base image directory.