paricafe/packages/frontend/src/scripts/upload.ts
かっこかり f5100cc81f
feat(frontend): アップロードするファイルの名前をランダム文字列にできるように (#13688)
* feat(frontend): アップロードするファイルの名前をランダム文字列にできるように

* Update Changelog

* refactor

* 設定項目を移動

* fix

* 「オリジナルのファイル名を保持」に変更

* 拡張子を付加するように
2024-04-13 12:51:37 +09:00

152 lines
4.6 KiB
TypeScript

/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { reactive, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { v4 as uuid } from 'uuid';
import { readAndCompressImage } from '@misskey-dev/browser-image-resizer';
import { getCompressionConfig } from './upload/compress-config.js';
import { defaultStore } from '@/store.js';
import { apiUrl } from '@/config.js';
import { $i } from '@/account.js';
import { alert } from '@/os.js';
import { i18n } from '@/i18n.js';
type Uploading = {
id: string;
name: string;
progressMax: number | undefined;
progressValue: number | undefined;
img: string;
};
export const uploads = ref<Uploading[]>([]);
const mimeTypeMap = {
'image/webp': 'webp',
'image/jpeg': 'jpg',
'image/png': 'png',
} as const;
export function uploadFile(
file: File,
folder?: any,
name?: string,
keepOriginal: boolean = defaultStore.state.keepOriginalUploading,
): Promise<Misskey.entities.DriveFile> {
if ($i == null) throw new Error('Not logged in');
if (folder && typeof folder === 'object') folder = folder.id;
return new Promise((resolve, reject) => {
const id = uuid();
const reader = new FileReader();
reader.onload = async (): Promise<void> => {
const filename = name ?? file.name ?? 'untitled';
const extension = filename.split('.').length > 1 ? '.' + filename.split('.').pop() : '';
const ctx = reactive<Uploading>({
id,
name: defaultStore.state.keepOriginalFilename ? filename : id + extension,
progressMax: undefined,
progressValue: undefined,
img: window.URL.createObjectURL(file),
});
uploads.value.push(ctx);
const config = !keepOriginal ? await getCompressionConfig(file) : undefined;
let resizedImage: Blob | undefined;
if (config) {
try {
const resized = await readAndCompressImage(file, config);
if (resized.size < file.size || file.type === 'image/webp') {
// The compression may not always reduce the file size
// (and WebP is not browser safe yet)
resizedImage = resized;
}
if (_DEV_) {
const saved = ((1 - resized.size / file.size) * 100).toFixed(2);
console.log(`Image compression: before ${file.size} bytes, after ${resized.size} bytes, saved ${saved}%`);
}
ctx.name = file.type !== config.mimeType ? `${ctx.name}.${mimeTypeMap[config.mimeType]}` : ctx.name;
} catch (err) {
console.error('Failed to resize image', err);
}
}
const formData = new FormData();
formData.append('i', $i.token);
formData.append('force', 'true');
formData.append('file', resizedImage ?? file);
formData.append('name', ctx.name);
if (folder) formData.append('folderId', folder);
const xhr = new XMLHttpRequest();
xhr.open('POST', apiUrl + '/drive/files/create', true);
xhr.onload = ((ev: ProgressEvent<XMLHttpRequest>) => {
if (xhr.status !== 200 || ev.target == null || ev.target.response == null) {
// TODO: 消すのではなくて(ネットワーク的なエラーなら)再送できるようにしたい
uploads.value = uploads.value.filter(x => x.id !== id);
if (xhr.status === 413) {
alert({
type: 'error',
title: i18n.ts.failedToUpload,
text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit,
});
} else if (ev.target?.response) {
const res = JSON.parse(ev.target.response);
if (res.error?.id === 'bec5bd69-fba3-43c9-b4fb-2894b66ad5d2') {
alert({
type: 'error',
title: i18n.ts.failedToUpload,
text: i18n.ts.cannotUploadBecauseInappropriate,
});
} else if (res.error?.id === 'd08dbc37-a6a9-463a-8c47-96c32ab5f064') {
alert({
type: 'error',
title: i18n.ts.failedToUpload,
text: i18n.ts.cannotUploadBecauseNoFreeSpace,
});
} else {
alert({
type: 'error',
title: i18n.ts.failedToUpload,
text: `${res.error?.message}\n${res.error?.code}\n${res.error?.id}`,
});
}
} else {
alert({
type: 'error',
title: 'Failed to upload',
text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(xhr.response)}`,
});
}
reject();
return;
}
const driveFile = JSON.parse(ev.target.response);
resolve(driveFile);
uploads.value = uploads.value.filter(x => x.id !== id);
}) as (ev: ProgressEvent<EventTarget>) => any;
xhr.upload.onprogress = ev => {
if (ev.lengthComputable) {
ctx.progressMax = ev.total;
ctx.progressValue = ev.loaded;
}
};
xhr.send(formData);
};
reader.readAsArrayBuffer(file);
});
}