diff --git a/package.json b/package.json
index bb48d060cd..e5eb6b81fc 100644
--- a/package.json
+++ b/package.json
@@ -108,6 +108,7 @@
 		"chai-http": "4.2.1",
 		"chalk": "2.4.2",
 		"commander": "2.19.0",
+		"content-disposition": "0.5.3",
 		"crc-32": "1.2.0",
 		"css-loader": "2.1.1",
 		"cssnano": "4.1.10",
diff --git a/src/misc/content-disposition.ts b/src/misc/content-disposition.ts
new file mode 100644
index 0000000000..9df7ed4688
--- /dev/null
+++ b/src/misc/content-disposition.ts
@@ -0,0 +1,6 @@
+const cd = require('content-disposition');
+
+export function contentDisposition(type: 'inline' | 'attachment', filename: string): string {
+	const fallback = filename.replace(/[^\w.-]/g, '_');
+	return cd(filename, { type, fallback });
+}
diff --git a/src/server/file/send-drive-file.ts b/src/server/file/send-drive-file.ts
index c57648bb7a..3f7533b82c 100644
--- a/src/server/file/send-drive-file.ts
+++ b/src/server/file/send-drive-file.ts
@@ -6,6 +6,7 @@ import DriveFile, { getDriveFileBucket } from '../../models/drive-file';
 import DriveFileThumbnail, { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail';
 import DriveFileWebpublic, { getDriveFileWebpublicBucket } from '../../models/drive-file-webpublic';
 import { serverLogger } from '..';
+import { contentDisposition } from '../../misc/content-disposition';
 
 const assets = `${__dirname}/../../server/file/assets/`;
 
@@ -63,12 +64,12 @@ export default async function(ctx: Koa.BaseContext) {
 
 		if (thumb != null) {
 			ctx.set('Content-Type', 'image/jpeg');
-			ctx.set('Content-Disposition', `filename="${rename(file.filename, { suffix: '-thumb', extname: '.jpeg' })}"`);
+			ctx.set('Content-Disposition', contentDisposition('inline', `${rename(file.filename, { suffix: '-thumb', extname: '.jpeg' })}"`));
 			const bucket = await getDriveFileThumbnailBucket();
 			ctx.body = bucket.openDownloadStream(thumb._id);
 		} else {
 			if (file.contentType.startsWith('image/')) {
-				ctx.set('Content-Disposition', `filename="${file.filename}"`);
+				ctx.set('Content-Disposition', contentDisposition('inline', `${file.filename}"`));
 				await sendRaw();
 			} else {
 				ctx.status = 404;
@@ -82,17 +83,17 @@ export default async function(ctx: Koa.BaseContext) {
 
 		if (web != null) {
 			ctx.set('Content-Type', file.contentType);
-			ctx.set('Content-Disposition', `filename="${rename(file.filename, { suffix: '-web' })}"`);
+			ctx.set('Content-Disposition', contentDisposition('inline', `${rename(file.filename, { suffix: '-web' })}"`));
 
 			const bucket = await getDriveFileWebpublicBucket();
 			ctx.body = bucket.openDownloadStream(web._id);
 		} else {
-			ctx.set('Content-Disposition', `filename="${file.filename}"`);
+			ctx.set('Content-Disposition', contentDisposition('inline', `${file.filename}"`));
 			await sendRaw();
 		}
 	} else {
 		if ('download' in ctx.query) {
-			ctx.set('Content-Disposition', `attachment; filename="${file.filename}`);
+			ctx.set('Content-Disposition', contentDisposition('attachment', `${file.filename}`));
 		}
 
 		await sendRaw();
diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts
index 0938b18415..5be71bc0a2 100644
--- a/src/services/drive/add-file.ts
+++ b/src/services/drive/add-file.ts
@@ -26,6 +26,7 @@ import { driveLogger } from './logger';
 import { IImage, ConvertToJpeg, ConvertToWebp, ConvertToPng } from './image-processor';
 import Instance from '../../models/instance';
 import checkSvg from '../../misc/check-svg';
+import { contentDisposition } from '../../misc/content-disposition';
 
 const logger = driveLogger.createSubLogger('register', 'yellow');
 
@@ -69,7 +70,7 @@ async function save(path: string, name: string, type: string, hash: string, size
 		//#region Uploads
 		logger.info(`uploading original: ${key}`);
 		const uploads = [
-			upload(key, fs.createReadStream(path), type)
+			upload(key, fs.createReadStream(path), type, name)
 		];
 
 		if (alts.webpublic) {
@@ -77,7 +78,7 @@ async function save(path: string, name: string, type: string, hash: string, size
 			webpublicUrl = `${ baseUrl }/${ webpublicKey }`;
 
 			logger.info(`uploading webpublic: ${webpublicKey}`);
-			uploads.push(upload(webpublicKey, alts.webpublic.data, alts.webpublic.type));
+			uploads.push(upload(webpublicKey, alts.webpublic.data, alts.webpublic.type, name));
 		}
 
 		if (alts.thumbnail) {
@@ -198,13 +199,17 @@ export async function generateAlts(path: string, type: string, generateWeb: bool
 /**
  * Upload to ObjectStorage
  */
-async function upload(key: string, stream: fs.ReadStream | Buffer, type: string) {
+async function upload(key: string, stream: fs.ReadStream | Buffer, type: string, filename?: string) {
 	const minio = new Minio.Client(config.drive.config);
 
-	await minio.putObject(config.drive.bucket, key, stream, null, {
+	const metadata = {
 		'Content-Type': type,
 		'Cache-Control': 'max-age=31536000, immutable'
-	});
+	} as Minio.ItemBucketMetadata;
+
+	if (filename) metadata['Content-Disposition'] = contentDisposition('inline', filename);
+
+	await minio.putObject(config.drive.bucket, key, stream, null, metadata);
 }
 
 /**