diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts
index 734d135648..9fdaf5eb86 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -36,7 +36,7 @@ import { GlobalEventService } from './GlobalEventService.js';
 import { HashtagService } from './HashtagService.js';
 import { HttpRequestService } from './HttpRequestService.js';
 import { IdService } from './IdService.js';
-import { ImageProcessingService } from './ImageProcessingService.js';
+import { __YUME_PRIVATE_ImageProcessingService } from './ImageProcessingService.js';
 import { InstanceActorService } from './InstanceActorService.js';
 import { InternalStorageService } from './InternalStorageService.js';
 import { MetaService } from './MetaService.js';
@@ -67,7 +67,7 @@ import { UserMutingService } from './UserMutingService.js';
 import { UserRenoteMutingService } from './UserRenoteMutingService.js';
 import { UserSuspendService } from './UserSuspendService.js';
 import { UserAuthService } from './UserAuthService.js';
-import { VideoProcessingService } from './VideoProcessingService.js';
+import { __YUME_PRIVATE_VideoProcessingService } from './VideoProcessingService.js';
 import { UserWebhookService } from './UserWebhookService.js';
 import { ProxyAccountService } from './ProxyAccountService.js';
 import { UtilityService } from './UtilityService.js';
@@ -179,7 +179,7 @@ const $GlobalEventService: Provider = { provide: 'GlobalEventService', useExisti
 const $HashtagService: Provider = { provide: 'HashtagService', useExisting: HashtagService };
 const $HttpRequestService: Provider = { provide: 'HttpRequestService', useExisting: HttpRequestService };
 const $IdService: Provider = { provide: 'IdService', useExisting: IdService };
-const $ImageProcessingService: Provider = { provide: 'ImageProcessingService', useExisting: ImageProcessingService };
+const $ImageProcessingService: Provider = { provide: '__YUME_PRIVATE_ImageProcessingService', useExisting: __YUME_PRIVATE_ImageProcessingService };
 const $InstanceActorService: Provider = { provide: 'InstanceActorService', useExisting: InstanceActorService };
 const $InternalStorageService: Provider = { provide: 'InternalStorageService', useExisting: InternalStorageService };
 const $MetaService: Provider = { provide: 'MetaService', useExisting: MetaService };
@@ -212,7 +212,7 @@ const $UserRenoteMutingService: Provider = { provide: 'UserRenoteMutingService',
 const $UserSearchService: Provider = { provide: 'UserSearchService', useExisting: UserSearchService };
 const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
 const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService };
-const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
+const $VideoProcessingService: Provider = { provide: '__YUME_PRIVATE_VideoProcessingService', useExisting: __YUME_PRIVATE_VideoProcessingService };
 const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService };
 const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService };
 const $WebhookTestService: Provider = { provide: 'WebhookTestService', useExisting: WebhookTestService };
@@ -330,7 +330,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		HashtagService,
 		HttpRequestService,
 		IdService,
-		ImageProcessingService,
+		__YUME_PRIVATE_ImageProcessingService,
 		InstanceActorService,
 		InternalStorageService,
 		MetaService,
@@ -363,7 +363,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		UserSearchService,
 		UserSuspendService,
 		UserAuthService,
-		VideoProcessingService,
+		__YUME_PRIVATE_VideoProcessingService,
 		UserWebhookService,
 		SystemWebhookService,
 		WebhookTestService,
@@ -625,7 +625,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		HashtagService,
 		HttpRequestService,
 		IdService,
-		ImageProcessingService,
+		__YUME_PRIVATE_ImageProcessingService,
 		InstanceActorService,
 		InternalStorageService,
 		MetaService,
@@ -658,7 +658,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		UserSearchService,
 		UserSuspendService,
 		UserAuthService,
-		VideoProcessingService,
+		__YUME_PRIVATE_VideoProcessingService,
 		UserWebhookService,
 		SystemWebhookService,
 		WebhookTestService,
diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts
index 495d67a93b..517a682753 100644
--- a/packages/backend/src/core/DriveService.ts
+++ b/packages/backend/src/core/DriveService.ts
@@ -22,8 +22,8 @@ import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
 import { IdentifiableError } from '@/misc/identifiable-error.js';
 import { contentDisposition } from '@/misc/content-disposition.js';
 import { GlobalEventService } from '@/core/GlobalEventService.js';
-import { VideoProcessingService } from '@/core/VideoProcessingService.js';
-import { ImageProcessingService } from '@/core/ImageProcessingService.js';
+import { __YUME_PRIVATE_VideoProcessingService } from '@/core/VideoProcessingService.js';
+import { __YUME_PRIVATE_ImageProcessingService } from '@/core/ImageProcessingService.js';
 import type { IImage } from '@/core/ImageProcessingService.js';
 import { QueueService } from '@/core/QueueService.js';
 import type { MiDriveFolder } from '@/models/DriveFolder.js';
@@ -120,8 +120,8 @@ export class DriveService {
 		private downloadService: DownloadService,
 		private internalStorageService: InternalStorageService,
 		private s3Service: S3Service,
-		private imageProcessingService: ImageProcessingService,
-		private videoProcessingService: VideoProcessingService,
+		private privateImageProcessingService: __YUME_PRIVATE_ImageProcessingService,
+		private privateVideoProcessingService: __YUME_PRIVATE_VideoProcessingService,
 		private globalEventService: GlobalEventService,
 		private queueService: QueueService,
 		private roleService: RoleService,
@@ -277,7 +277,7 @@ export class DriveService {
 			}
 
 			try {
-				const thumbnail = await this.videoProcessingService.generateVideoThumbnail(path);
+				const thumbnail = await this.privateVideoProcessingService.generateVideoThumbnail(path);
 				return {
 					webpublic: null,
 					thumbnail,
@@ -331,9 +331,9 @@ export class DriveService {
 
 			try {
 				if (['image/jpeg', 'image/webp', 'image/avif'].includes(type)) {
-					webpublic = await this.imageProcessingService.convertSharpToWebp(img, 2048, 2048);
+					webpublic = await this.privateImageProcessingService.convertSharpToWebp(img, 2048, 2048);
 				} else if (['image/png', 'image/bmp', 'image/svg+xml'].includes(type)) {
-					webpublic = await this.imageProcessingService.convertSharpToPng(img, 2048, 2048);
+					webpublic = await this.privateImageProcessingService.convertSharpToPng(img, 2048, 2048);
 				} else {
 					this.registerLogger.debug('web image not created (not an required image)');
 				}
@@ -352,9 +352,9 @@ export class DriveService {
 
 		try {
 			if (isAnimated) {
-				thumbnail = await this.imageProcessingService.convertSharpToWebp(sharp(path, { animated: true }), 374, 317, { alphaQuality: 70 });
+				thumbnail = await this.privateImageProcessingService.convertSharpToWebp(sharp(path, { animated: true }), 374, 317, { alphaQuality: 70 });
 			} else {
-				thumbnail = await this.imageProcessingService.convertSharpToWebp(img, 498, 422);
+				thumbnail = await this.privateImageProcessingService.convertSharpToWebp(img, 498, 422);
 			}
 		} catch (err) {
 			this.registerLogger.warn('thumbnail not created (an error occurred)', err as Error);
diff --git a/packages/backend/src/core/ImageProcessingService.ts b/packages/backend/src/core/ImageProcessingService.ts
index 6f978b34c8..6aa25decc8 100644
--- a/packages/backend/src/core/ImageProcessingService.ts
+++ b/packages/backend/src/core/ImageProcessingService.ts
@@ -46,7 +46,9 @@ import { bindThis } from '@/decorators.js';
 import { Readable } from 'node:stream';
 
 @Injectable()
-export class ImageProcessingService {
+// Prevent accidental import by upstream merge
+// eslint-disable-next-line
+export class __YUME_PRIVATE_ImageProcessingService {
 	constructor(
 	) {
 	}
diff --git a/packages/backend/src/core/VideoProcessingService.ts b/packages/backend/src/core/VideoProcessingService.ts
index 747fe4fc7e..461e427b0d 100644
--- a/packages/backend/src/core/VideoProcessingService.ts
+++ b/packages/backend/src/core/VideoProcessingService.ts
@@ -7,19 +7,21 @@ import { Inject, Injectable } from '@nestjs/common';
 import FFmpeg from 'fluent-ffmpeg';
 import { DI } from '@/di-symbols.js';
 import type { Config } from '@/config.js';
-import { ImageProcessingService } from '@/core/ImageProcessingService.js';
+import { __YUME_PRIVATE_ImageProcessingService } from '@/core/ImageProcessingService.js';
 import type { IImage } from '@/core/ImageProcessingService.js';
 import { createTempDir } from '@/misc/create-temp.js';
 import { bindThis } from '@/decorators.js';
 import { appendQuery, query } from '@/misc/prelude/url.js';
 
 @Injectable()
-export class VideoProcessingService {
+// Prevent accidental import by upstream merge
+// eslint-disable-next-line
+export class __YUME_PRIVATE_VideoProcessingService {
 	constructor(
 		@Inject(DI.config)
 		private config: Config,
 
-		private imageProcessingService: ImageProcessingService,
+		private imageProcessingService: __YUME_PRIVATE_ImageProcessingService,
 	) {
 	}
 
diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts
index c485555f90..a1dbef36da 100644
--- a/packages/backend/src/core/entities/DriveFileEntityService.ts
+++ b/packages/backend/src/core/entities/DriveFileEntityService.ts
@@ -18,7 +18,6 @@ import { bindThis } from '@/decorators.js';
 import { isMimeImage } from '@/misc/is-mime-image.js';
 import { IdService } from '@/core/IdService.js';
 import { UtilityService } from '../UtilityService.js';
-import { VideoProcessingService } from '../VideoProcessingService.js';
 import { UserEntityService } from './UserEntityService.js';
 import { DriveFolderEntityService } from './DriveFolderEntityService.js';
 
@@ -43,7 +42,6 @@ export class DriveFileEntityService {
 
 		private utilityService: UtilityService,
 		private driveFolderEntityService: DriveFolderEntityService,
-		private videoProcessingService: VideoProcessingService,
 		private idService: IdService,
 	) {
 	}
@@ -86,11 +84,7 @@ export class DriveFileEntityService {
 
 	@bindThis
 	public getThumbnailUrl(file: MiDriveFile): string | null {
-		if (file.type.startsWith('video')) {
-			if (file.thumbnailUrl) return file.thumbnailUrl;
-
-			return this.videoProcessingService.getExternalVideoThumbnailUrl(file.webpublicUrl ?? file.url);
-		} else if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) {
+		if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) {
 			// 動画ではなくリモートかつメディアプロキシ
 			return this.getProxiedUrl(file.uri, 'static');
 		}
diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts
index 91d826382d..5b10faa89f 100644
--- a/packages/backend/src/server/FileServerService.ts
+++ b/packages/backend/src/server/FileServerService.ts
@@ -8,27 +8,19 @@ import { fileURLToPath } from 'node:url';
 import { dirname } from 'node:path';
 import { Inject, Injectable } from '@nestjs/common';
 import rename from 'rename';
-import sharp from 'sharp';
-import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
 import type { Config } from '@/config.js';
 import type { MiDriveFile, DriveFilesRepository } from '@/models/_.js';
 import { DI } from '@/di-symbols.js';
-import { createTemp } from '@/misc/create-temp.js';
 import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
 import { StatusError } from '@/misc/status-error.js';
 import type Logger from '@/logger.js';
-import { DownloadService } from '@/core/DownloadService.js';
-import { IImageStreamable, ImageProcessingService, webpDefault } from '@/core/ImageProcessingService.js';
-import { VideoProcessingService } from '@/core/VideoProcessingService.js';
-import { InternalStorageService } from '@/core/InternalStorageService.js';
 import { contentDisposition } from '@/misc/content-disposition.js';
 import { FileInfoService } from '@/core/FileInfoService.js';
 import { LoggerService } from '@/core/LoggerService.js';
 import { bindThis } from '@/decorators.js';
-import { isMimeImage } from '@/misc/is-mime-image.js';
-import { correctFilename } from '@/misc/correct-filename.js';
 import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
 import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
+import { InternalStorageService } from '@/core/InternalStorageService.js';
 
 const _filename = fileURLToPath(import.meta.url);
 const _dirname = dirname(_filename);
@@ -46,11 +38,8 @@ export class FileServerService {
 		@Inject(DI.driveFilesRepository)
 		private driveFilesRepository: DriveFilesRepository,
 
-		private fileInfoService: FileInfoService,
-		private downloadService: DownloadService,
-		private imageProcessingService: ImageProcessingService,
-		private videoProcessingService: VideoProcessingService,
 		private internalStorageService: InternalStorageService,
+		private fileInfoService: FileInfoService,
 		private loggerService: LoggerService,
 	) {
 		this.logger = this.loggerService.getLogger('server', 'gray');
@@ -134,159 +123,72 @@ export class FileServerService {
 			return;
 		}
 
-		try {
-			if (file.state === 'remote') {
-				let image: IImageStreamable | null = null;
+		if (file.state === 'remote') {
+			const url = new URL(`${this.config.mediaProxy}/`);
 
-				if (file.fileRole === 'thumbnail') {
-					if (isMimeImage(file.mime, 'sharp-convertible-image-with-bmp')) {
-						reply.header('Cache-Control', 'max-age=31536000, immutable');
+			url.searchParams.set('url', file.url);
 
-						const url = new URL(`${this.config.mediaProxy}/static.webp`);
-						url.searchParams.set('url', file.url);
-						url.searchParams.set('static', '1');
+			return await reply.redirect(url.toString(), 301);
+		}
 
-						file.cleanup();
-						return await reply.redirect(url.toString(), 301);
-					} else if (file.mime.startsWith('video/')) {
-						const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url);
-						if (externalThumbnail) {
-							file.cleanup();
-							return await reply.redirect(externalThumbnail, 301);
-						}
+		if (file.fileRole !== 'original') {
+			const filename = rename(file.filename, {
+				suffix: file.fileRole === 'thumbnail' ? '-thumb' : '-web',
+				extname: file.ext ? `.${file.ext}` : '.unknown',
+			}).toString();
 
-						image = await this.videoProcessingService.generateVideoThumbnail(file.path);
-					}
+			reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.mime) ? file.mime : 'application/octet-stream');
+			reply.header('Cache-Control', 'max-age=31536000, immutable');
+			reply.header('Content-Disposition', contentDisposition('inline', filename));
+
+			if (request.headers.range && file.file.size > 0) {
+				const range = request.headers.range as string;
+				const parts = range.replace(/bytes=/, '').split('-');
+				const start = parseInt(parts[0], 10);
+				let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
+				if (end > file.file.size) {
+					end = file.file.size - 1;
 				}
-
-				if (file.fileRole === 'webpublic') {
-					if (['image/svg+xml'].includes(file.mime)) {
-						reply.header('Cache-Control', 'max-age=31536000, immutable');
-
-						const url = new URL(`${this.config.mediaProxy}/svg.webp`);
-						url.searchParams.set('url', file.url);
-
-						file.cleanup();
-						return await reply.redirect(url.toString(), 301);
-					}
-				}
-
-				if (!image) {
-					if (request.headers.range && file.file.size > 0) {
-						const range = request.headers.range as string;
-						const parts = range.replace(/bytes=/, '').split('-');
-						const start = parseInt(parts[0], 10);
-						let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
-						if (end > file.file.size) {
-							end = file.file.size - 1;
-						}
-						const chunksize = end - start + 1;
-
-						image = {
-							data: fs.createReadStream(file.path, {
-								start,
-								end,
-							}),
-							ext: file.ext,
-							type: file.mime,
-						};
-
-						reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
-						reply.header('Accept-Ranges', 'bytes');
-						reply.header('Content-Length', chunksize);
-						reply.code(206);
-					} else {
-						image = {
-							data: fs.createReadStream(file.path),
-							ext: file.ext,
-							type: file.mime,
-						};
-					}
-				}
-
-				if ('pipe' in image.data && typeof image.data.pipe === 'function') {
-					// image.dataがstreamなら、stream終了後にcleanup
-					image.data.on('end', file.cleanup);
-					image.data.on('close', file.cleanup);
-				} else {
-					// image.dataがstreamでないなら直ちにcleanup
-					file.cleanup();
-				}
-
-				reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream');
-				reply.header('Content-Length', file.file.size);
-				reply.header('Cache-Control', 'max-age=31536000, immutable');
-				reply.header('Content-Disposition',
-					contentDisposition(
-						'inline',
-						correctFilename(file.filename, image.ext),
-					),
-				);
-				return image.data;
+				const chunksize = end - start + 1;
+				const fileStream = fs.createReadStream(file.path, {
+					start,
+					end,
+				});
+				reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
+				reply.header('Accept-Ranges', 'bytes');
+				reply.header('Content-Length', chunksize);
+				reply.code(206);
+				return fileStream;
 			}
 
-			if (file.fileRole !== 'original') {
-				const filename = rename(file.filename, {
-					suffix: file.fileRole === 'thumbnail' ? '-thumb' : '-web',
-					extname: file.ext ? `.${file.ext}` : '.unknown',
-				}).toString();
+			return fs.createReadStream(file.path);
+		} else {
+			reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.file.type) ? file.file.type : 'application/octet-stream');
+			reply.header('Content-Length', file.file.size);
+			reply.header('Cache-Control', 'max-age=31536000, immutable');
+			reply.header('Content-Disposition', contentDisposition('inline', file.filename));
 
-				reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.mime) ? file.mime : 'application/octet-stream');
-				reply.header('Cache-Control', 'max-age=31536000, immutable');
-				reply.header('Content-Disposition', contentDisposition('inline', filename));
-
-				if (request.headers.range && file.file.size > 0) {
-					const range = request.headers.range as string;
-					const parts = range.replace(/bytes=/, '').split('-');
-					const start = parseInt(parts[0], 10);
-					let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
-					if (end > file.file.size) {
-						end = file.file.size - 1;
-					}
-					const chunksize = end - start + 1;
-					const fileStream = fs.createReadStream(file.path, {
-						start,
-						end,
-					});
-					reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
-					reply.header('Accept-Ranges', 'bytes');
-					reply.header('Content-Length', chunksize);
-					reply.code(206);
-					return fileStream;
+			if (request.headers.range && file.file.size > 0) {
+				const range = request.headers.range as string;
+				const parts = range.replace(/bytes=/, '').split('-');
+				const start = parseInt(parts[0], 10);
+				let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
+				if (end > file.file.size) {
+					end = file.file.size - 1;
 				}
-
-				return fs.createReadStream(file.path);
-			} else {
-				reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.file.type) ? file.file.type : 'application/octet-stream');
-				reply.header('Content-Length', file.file.size);
-				reply.header('Cache-Control', 'max-age=31536000, immutable');
-				reply.header('Content-Disposition', contentDisposition('inline', file.filename));
-
-				if (request.headers.range && file.file.size > 0) {
-					const range = request.headers.range as string;
-					const parts = range.replace(/bytes=/, '').split('-');
-					const start = parseInt(parts[0], 10);
-					let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
-					if (end > file.file.size) {
-						end = file.file.size - 1;
-					}
-					const chunksize = end - start + 1;
-					const fileStream = fs.createReadStream(file.path, {
-						start,
-						end,
-					});
-					reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
-					reply.header('Accept-Ranges', 'bytes');
-					reply.header('Content-Length', chunksize);
-					reply.code(206);
-					return fileStream;
-				}
-
-				return fs.createReadStream(file.path);
+				const chunksize = end - start + 1;
+				const fileStream = fs.createReadStream(file.path, {
+					start,
+					end,
+				});
+				reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
+				reply.header('Accept-Ranges', 'bytes');
+				reply.header('Content-Length', chunksize);
+				reply.code(206);
+				return fileStream;
 			}
-		} catch (e) {
-			if ('cleanup' in file) file.cleanup();
-			throw e;
+
+			return fs.createReadStream(file.path);
 		}
 	}
 
@@ -302,234 +204,56 @@ export class FileServerService {
 		// アバタークロップなど、どうしてもオリジンである必要がある場合
 		const mustOrigin = 'origin' in request.query;
 
-		if (this.config.externalMediaProxyEnabled) {
-			// 外部のメディアプロキシが有効なら、そちらにリダイレクト
-
-			reply.header('Cache-Control', 'public, max-age=259200'); // 3 days
-
-			const externalURL = new URL(`${this.config.mediaProxy}/${request.params.url || ''}`);
-
-			for (const [key, value] of Object.entries(request.query)) {
-				externalURL.searchParams.append(key, value);
-			}
-
-			if (mustOrigin) {
-				url = `${this.config.mediaProxy}?url=${encodeURIComponent(url)}`;
-			} else {
-				return await reply.redirect(
-					externalURL.toString(),
-					301,
-				);
-			}
+		if (!this.config.mediaProxy) {
+			reply.code(501);
 		}
 
-		if (!request.headers['user-agent']) {
-			throw new StatusError('User-Agent is required', 400, 'User-Agent is required');
-		} else if (request.headers['user-agent'].toLowerCase().indexOf('misskey/') !== -1) {
-			throw new StatusError('Refusing to proxy a request from another proxy', 403, 'Proxy is recursive');
+		reply.header('Cache-Control', 'public, max-age=259200'); // 3 days
+
+		const externalURL = new URL(`${this.config.mediaProxy}/${request.params.url || ''}`);
+
+		for (const [key, value] of Object.entries(request.query)) {
+			externalURL.searchParams.append(key, value);
 		}
 
-		if (!request.headers['user-agent']) {
-			throw new StatusError('User-Agent is required', 400, 'User-Agent is required');
-		} else if (request.headers['user-agent'].toLowerCase().indexOf('misskey/') !== -1) {
-			throw new StatusError('Refusing to proxy a request from another proxy', 403, 'Proxy is recursive');
-		}
-
-		// Create temp file
-		const file = await this.getStreamAndTypeFromUrl(url);
-		if (file === '404') {
-			reply.code(404);
-			reply.header('Cache-Control', 'max-age=86400');
-			return reply.sendFile('/dummy.png', assets);
-		}
-
-		if (file === '204') {
-			reply.code(204);
-			reply.header('Cache-Control', 'max-age=86400');
-			return;
-		}
-
-		try {
-			const isConvertibleImage = isMimeImage(file.mime, 'sharp-convertible-image-with-bmp');
-			const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image-with-bmp');
-
-			if (
-				'emoji' in request.query ||
-				'avatar' in request.query ||
-				'static' in request.query ||
-				'preview' in request.query ||
-				'badge' in request.query
-			) {
-				if (!isConvertibleImage) {
-					// 画像でないなら404でお茶を濁す
-					throw new StatusError('Unexpected mime', 404);
-				}
-			}
-
-			let image: IImageStreamable | null = null;
-			if ('emoji' in request.query || 'avatar' in request.query) {
-				if (!isAnimationConvertibleImage && !('static' in request.query)) {
-					image = {
-						data: fs.createReadStream(file.path),
-						ext: file.ext,
-						type: file.mime,
-					};
-				} else {
-					const data = (await sharpBmp(file.path, file.mime, { animated: !('static' in request.query) }))
-						.resize({
-							height: 'emoji' in request.query ? 128 : 320,
-							withoutEnlargement: true,
-						})
-						.webp(webpDefault);
-
-					image = {
-						data,
-						ext: 'webp',
-						type: 'image/webp',
-					};
-				}
-			} else if ('static' in request.query) {
-				image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 498, 422);
-			} else if ('preview' in request.query) {
-				image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 200, 200);
-			} else if ('badge' in request.query) {
-				const mask = (await sharpBmp(file.path, file.mime))
-					.resize(96, 96, {
-						fit: 'contain',
-						position: 'centre',
-						withoutEnlargement: false,
-					})
-					.greyscale()
-					.normalise()
-					.linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast
-					.flatten({ background: '#000' })
-					.toColorspace('b-w');
-
-				const stats = await mask.clone().stats();
-
-				if (stats.entropy < 0.1) {
-					// エントロピーがあまりない場合は404にする
-					throw new StatusError('Skip to provide badge', 404);
-				}
-
-				const data = sharp({
-					create: { width: 96, height: 96, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } },
-				})
-					.pipelineColorspace('b-w')
-					.boolean(await mask.png().toBuffer(), 'eor');
-
-				image = {
-					data: await data.png().toBuffer(),
-					ext: 'png',
-					type: 'image/png',
-				};
-			} else if (file.mime === 'image/svg+xml') {
-				image = this.imageProcessingService.convertToWebpStream(file.path, 2048, 2048);
-			} else if (!file.mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(file.mime)) {
-				throw new StatusError('Rejected type', 403, 'Rejected type');
-			}
-
-			if (!image) {
-				if (request.headers.range && file.file && file.file.size > 0) {
-					const range = request.headers.range as string;
-					const parts = range.replace(/bytes=/, '').split('-');
-					const start = parseInt(parts[0], 10);
-					let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
-					if (end > file.file.size) {
-						end = file.file.size - 1;
-					}
-					const chunksize = end - start + 1;
-
-					image = {
-						data: fs.createReadStream(file.path, {
-							start,
-							end,
-						}),
-						ext: file.ext,
-						type: file.mime,
-					};
-
-					reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
-					reply.header('Accept-Ranges', 'bytes');
-					reply.header('Content-Length', chunksize);
-					reply.code(206);
-				} else {
-					image = {
-						data: fs.createReadStream(file.path),
-						ext: file.ext,
-						type: file.mime,
-					};
-				}
-			}
-
-			if ('cleanup' in file) {
-				if ('pipe' in image.data && typeof image.data.pipe === 'function') {
-					// image.dataがstreamなら、stream終了後にcleanup
-					image.data.on('end', file.cleanup);
-					image.data.on('close', file.cleanup);
-				} else {
-					// image.dataがstreamでないなら直ちにcleanup
-					file.cleanup();
-				}
-			}
-
-			reply.header('Content-Type', image.type);
-			reply.header('Cache-Control', 'max-age=31536000, immutable');
-			reply.header('Content-Disposition',
-				contentDisposition(
-					'inline',
-					correctFilename(file.filename, image.ext),
-				),
+		if (mustOrigin) {
+			url = `${this.config.mediaProxy}?url=${encodeURIComponent(url)}`;
+		} else {
+			return await reply.redirect(
+				externalURL.toString(),
+				301,
 			);
-			return image.data;
-		} catch (e) {
-			if ('cleanup' in file) file.cleanup();
-			throw e;
-		}
-	}
-
-	@bindThis
-	private async getStreamAndTypeFromUrl(url: string): Promise<
-		{ state: 'remote'; fileRole?: 'thumbnail' | 'webpublic' | 'original'; file?: MiDriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; }
-		| { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: MiDriveFile; filename: string; mime: string; ext: string | null; path: string; }
-		| '404'
-		| '204'
-	> {
-		if (url.startsWith(`${this.config.url}/files/`)) {
-			const key = url.replace(`${this.config.url}/files/`, '').split('/').shift();
-			if (!key) throw new StatusError('Invalid File Key', 400, 'Invalid File Key');
-
-			return await this.getFileFromKey(key);
 		}
 
-		return await this.downloadAndDetectTypeFromUrl(url);
-	}
-
-	@bindThis
-	private async downloadAndDetectTypeFromUrl(url: string): Promise<
-		{ state: 'remote' ; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; }
-	> {
-		const [path, cleanup] = await createTemp();
-		try {
-			const { filename } = await this.downloadService.downloadUrl(url, path);
-
-			const { mime, ext } = await this.fileInfoService.detectType(path);
-
-			return {
-				state: 'remote',
-				mime, ext,
-				path, cleanup,
-				filename,
-			};
-		} catch (e) {
-			cleanup();
-			throw e;
+		if (!request.headers['user-agent']) {
+			throw new StatusError('User-Agent is required', 400, 'User-Agent is required');
+		} else if (request.headers['user-agent'].toLowerCase().indexOf('misskey/') !== -1) {
+			throw new StatusError('Refusing to proxy a request from another proxy', 403, 'Proxy is recursive');
 		}
+
+		if (!request.headers['user-agent']) {
+			throw new StatusError('User-Agent is required', 400, 'User-Agent is required');
+		} else if (request.headers['user-agent'].toLowerCase().indexOf('misskey/') !== -1) {
+			throw new StatusError('Refusing to proxy a request from another proxy', 403, 'Proxy is recursive');
+		}
+
+		// directly proxy request through
+		const res = await fetch(url, {
+			headers: {
+				'User-Agent': request.headers['user-agent'],
+			},
+		});
+
+		reply.code(res.status);
+		for (const [key, value] of res.headers.entries()) {
+			reply.header(key, value);
+		}
+		reply.send(res.body);
 	}
 
 	@bindThis
 	private async getFileFromKey(key: string): Promise<
-		{ state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: MiDriveFile; filename: string; url: string; mime: string; ext: string | null; path: string; cleanup: () => void; }
+		{ state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; filename: string; url: string; }
 		| { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: MiDriveFile; filename: string; mime: string; ext: string | null; path: string; }
 		| '404'
 		| '204'
@@ -548,15 +272,10 @@ export class FileServerService {
 
 		if (!file.storedInternal) {
 			if (!(file.isLink && file.uri)) return '204';
-			const result = await this.downloadAndDetectTypeFromUrl(file.uri);
-			file.size = (await fs.promises.stat(result.path)).size;	// DB file.sizeは正確とは限らないので
-			return {
-				...result,
-				url: file.uri,
-				fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original',
-				file,
-				filename: file.name,
-			};
+			return { state: 'remote', 
+					fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original', 
+					filename: file.name 
+					, url: file.uri };
 		}
 
 		const path = this.internalStorageService.resolvePath(key);