From 1af98b690ba7cccb14c524df83c5bb297896a38f Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Sat, 3 May 2025 12:57:50 +0900 Subject: [PATCH 1/5] feat: CREATE INDEX CONCURRENTLY for "userId" "id" composite note index if admin wish. (#15915) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: CREATE INDEX CONCURRENTLY for "userId" "id" composite note index * chore: remove { concurrent: true } and comment why * update comment * feat: add MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY option * fix: spdx license header * alter comment * chore: improve behavior when migration failure * docs(changelog): 2025.4.1 で追加されたインデックスの再生成をノートの追加しながら行えるようになりました * ちょっと表現を変更 --------- Co-authored-by: 饺子w (Yumechi) <35571479+eternal-flame-ad@users.noreply.github.com> --- CHANGELOG.md | 5 +++++ .../1745378064470-composite-note-index.js | 19 +++++++++++++++++-- .../backend/migration/js/migration-config.js | 8 ++++++++ packages/backend/ormconfig.js | 2 ++ packages/backend/src/GlobalModule.ts | 9 +++++++-- packages/backend/src/models/Note.ts | 10 ++++++++++ 6 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 packages/backend/migration/js/migration-config.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 92c3fada72..dffdc51f7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ ### Server - Enhance: 凍結されたユーザのノートが各種タイムラインで表示されないように `#15775` - Enhance: 連合先のソフトウェア及びバージョン名により配信停止を行えるように `#15727` +- Enhance: 2025.4.1 で追加されたインデックスの再生成をノートの追加しながら行えるようになりました。 `#15915` + - `MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY` 環境変数を `1` にセットしていると、巨大なテーブルの既存のカラムに関するインデックス再生成が`CREATE INDEX CONCURRENTLY`を使用するようになりました。 + - 複数のサーバープロセスをクラスタリングしているサーバーにおいて、一部のプロセスが起動している状態でこのオプションを有効にしてマイグレーションすることにより、ダウンタイムを削減することができます。 + - ただし、このオプションを有効にする場合、インデックスの作成にかかる時間が倍~3倍以上になることがあります。 + - また、大きなインスタンスである場合にはインデックスの作成に失敗し、複数回再試行する必要がある可能性があります。 ## 2025.4.1 diff --git a/packages/backend/migration/1745378064470-composite-note-index.js b/packages/backend/migration/1745378064470-composite-note-index.js index 49e835d38c..1487aa9630 100644 --- a/packages/backend/migration/1745378064470-composite-note-index.js +++ b/packages/backend/migration/1745378064470-composite-note-index.js @@ -3,11 +3,25 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { isConcurrentIndexMigrationEnabled } from "./js/migration-config.js"; + export class CompositeNoteIndex1745378064470 { name = 'CompositeNoteIndex1745378064470'; + transaction = isConcurrentIndexMigrationEnabled() ? false : undefined; async up(queryRunner) { - await queryRunner.query(`CREATE INDEX "IDX_724b311e6f883751f261ebe378" ON "note" ("userId", "id" DESC)`); + const concurrently = isConcurrentIndexMigrationEnabled(); + + if (concurrently) { + const hasValidIndex = await queryRunner.query(`SELECT indisvalid FROM pg_index INNER JOIN pg_class ON pg_index.indexrelid = pg_class.oid WHERE pg_class.relname = 'IDX_724b311e6f883751f261ebe378'`); + if (!hasValidIndex || hasValidIndex[0].indisvalid !== true) { + await queryRunner.query(`DROP INDEX IF EXISTS "IDX_724b311e6f883751f261ebe378"`); + await queryRunner.query(`CREATE INDEX CONCURRENTLY "IDX_724b311e6f883751f261ebe378" ON "note" ("userId", "id" DESC)`); + } + } else { + await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_724b311e6f883751f261ebe378" ON "note" ("userId", "id" DESC)`); + } + await queryRunner.query(`DROP INDEX IF EXISTS "IDX_5b87d9d19127bd5d92026017a7"`); // Flush all cached Linear Scan Plans and redo statistics for composite index // this is important for Postgres to learn that even in highly complex queries, using this index first can reduce the result set significantly @@ -15,7 +29,8 @@ export class CompositeNoteIndex1745378064470 { } async down(queryRunner) { + const mayConcurrently = isConcurrentIndexMigrationEnabled() ? 'CONCURRENTLY' : ''; await queryRunner.query(`DROP INDEX IF EXISTS "IDX_724b311e6f883751f261ebe378"`); - await queryRunner.query(`CREATE INDEX "IDX_5b87d9d19127bd5d92026017a7" ON "note" ("userId")`); + await queryRunner.query(`CREATE INDEX ${mayConcurrently} "IDX_5b87d9d19127bd5d92026017a7" ON "note" ("userId")`); } } diff --git a/packages/backend/migration/js/migration-config.js b/packages/backend/migration/js/migration-config.js new file mode 100644 index 0000000000..8cfbb21470 --- /dev/null +++ b/packages/backend/migration/js/migration-config.js @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export function isConcurrentIndexMigrationEnabled() { + return process.env.MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY === '1'; +} diff --git a/packages/backend/ormconfig.js b/packages/backend/ormconfig.js index 229e5bf1fe..f979c36ad7 100644 --- a/packages/backend/ormconfig.js +++ b/packages/backend/ormconfig.js @@ -1,6 +1,7 @@ import { DataSource } from 'typeorm'; import { loadConfig } from './built/config.js'; import { entities } from './built/postgres.js'; +import { isConcurrentIndexMigrationEnabled } from "./migration/js/migration-config.js"; const config = loadConfig(); @@ -14,4 +15,5 @@ export default new DataSource({ extra: config.db.extra, entities: entities, migrations: ['migration/*.js'], + migrationsTransactionMode: isConcurrentIndexMigrationEnabled() ? 'each' : 'all', }); diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts index 5544eeeddd..435bd8dd45 100644 --- a/packages/backend/src/GlobalModule.ts +++ b/packages/backend/src/GlobalModule.ts @@ -24,8 +24,13 @@ const $config: Provider = { const $db: Provider = { provide: DI.db, useFactory: async (config) => { - const db = createPostgresDataSource(config); - return await db.initialize(); + try { + const db = createPostgresDataSource(config); + return await db.initialize(); + } catch (e) { + console.log(e); + throw e; + } }, inject: [DI.config], }; diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/Note.ts index abaf615bcf..3dcbdb735b 100644 --- a/packages/backend/src/models/Note.ts +++ b/packages/backend/src/models/Note.ts @@ -10,6 +10,16 @@ import { MiUser } from './User.js'; import { MiChannel } from './Channel.js'; import type { MiDriveFile } from './DriveFile.js'; +// Note: When you create a new index for existing column of this table, +// it might be better to index concurrently under isConcurrentIndexMigrationEnabled flag +// by editing generated migration file since this table is very large, +// and it will make a long lock to create index in most cases. +// Please note that `CREATE INDEX CONCURRENTLY` is not supported in transaction, +// so you need to set `transaction = false` in migration if isConcurrentIndexMigrationEnabled() is true. +// Please refer 1745378064470-composite-note-index.js for example. +// You should not use `@Index({ concurrent: true })` decorator because database initialization for test will fail +// because it will always run CREATE INDEX in transaction based on decorators. +// Not appending `{ concurrent: true }` to `@Index` will not cause any problem in production, @Index(['userId', 'id']) @Entity('note') export class MiNote { From c13aa0c2247d8eb8dc4c9f43099cd4e1eae71c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?= <46447427+samunohito@users.noreply.github.com> Date: Sat, 3 May 2025 15:40:57 +0900 Subject: [PATCH 2/5] =?UTF-8?q?fix(backend):=20=E3=83=81=E3=83=A3=E3=83=B3?= =?UTF-8?q?=E3=83=8D=E3=83=AB=E3=83=95=E3=82=A9=E3=83=AD=E3=83=BC=E4=B8=80?= =?UTF-8?q?=E8=A6=A7=E3=81=AEsinceId/untilId=E3=81=AB=E3=82=88=E3=82=8B?= =?UTF-8?q?=E7=B5=9E=E3=82=8A=E8=BE=BC=E3=81=BF=E3=81=8C=E4=B8=8A=E6=89=8B?= =?UTF-8?q?=E3=81=8F=E5=8B=95=E3=81=84=E3=81=A6=E3=81=84=E3=81=AA=E3=81=84?= =?UTF-8?q?=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3=20(#13698)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(backend): チャンネルフォロー一覧のsinceId/untilIdによる絞り込みが上手く動いていないのを修正 * fix CHANGELOG.md * docs(changelog): fix mistaken changelog insertion (restore newline) * docs(changelog): update insertion position --------- Co-authored-by: Sayamame-beans <61457993+Sayamame-beans@users.noreply.github.com> --- CHANGELOG.md | 1 + packages/backend/src/core/QueryService.ts | 39 +++++++++++-------- .../server/api/endpoints/channels/followed.ts | 10 ++++- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dffdc51f7c..dcae039ae6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - 複数のサーバープロセスをクラスタリングしているサーバーにおいて、一部のプロセスが起動している状態でこのオプションを有効にしてマイグレーションすることにより、ダウンタイムを削減することができます。 - ただし、このオプションを有効にする場合、インデックスの作成にかかる時間が倍~3倍以上になることがあります。 - また、大きなインスタンスである場合にはインデックスの作成に失敗し、複数回再試行する必要がある可能性があります。 +- Fix: チャンネルのフォロー一覧の結果が一部正しくないのを修正 (#12175) ## 2025.4.1 diff --git a/packages/backend/src/core/QueryService.ts b/packages/backend/src/core/QueryService.ts index e219efaf3d..b9cef5b0ec 100644 --- a/packages/backend/src/core/QueryService.ts +++ b/packages/backend/src/core/QueryService.ts @@ -43,29 +43,36 @@ export class QueryService { ) { } - public makePaginationQuery(q: SelectQueryBuilder, sinceId?: string | null, untilId?: string | null, sinceDate?: number | null, untilDate?: number | null): SelectQueryBuilder { + public makePaginationQuery( + q: SelectQueryBuilder, + sinceId?: string | null, + untilId?: string | null, + sinceDate?: number | null, + untilDate?: number | null, + targetColumn = 'id', + ): SelectQueryBuilder { if (sinceId && untilId) { - q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId }); - q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId }); - q.orderBy(`${q.alias}.id`, 'DESC'); + q.andWhere(`${q.alias}.${targetColumn} > :sinceId`, { sinceId: sinceId }); + q.andWhere(`${q.alias}.${targetColumn} < :untilId`, { untilId: untilId }); + q.orderBy(`${q.alias}.${targetColumn}`, 'DESC'); } else if (sinceId) { - q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId }); - q.orderBy(`${q.alias}.id`, 'ASC'); + q.andWhere(`${q.alias}.${targetColumn} > :sinceId`, { sinceId: sinceId }); + q.orderBy(`${q.alias}.${targetColumn}`, 'ASC'); } else if (untilId) { - q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId }); - q.orderBy(`${q.alias}.id`, 'DESC'); + q.andWhere(`${q.alias}.${targetColumn} < :untilId`, { untilId: untilId }); + q.orderBy(`${q.alias}.${targetColumn}`, 'DESC'); } else if (sinceDate && untilDate) { - q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: this.idService.gen(sinceDate) }); - q.andWhere(`${q.alias}.id < :untilId`, { untilId: this.idService.gen(untilDate) }); - q.orderBy(`${q.alias}.id`, 'DESC'); + q.andWhere(`${q.alias}.${targetColumn} > :sinceId`, { sinceId: this.idService.gen(sinceDate) }); + q.andWhere(`${q.alias}.${targetColumn} < :untilId`, { untilId: this.idService.gen(untilDate) }); + q.orderBy(`${q.alias}.${targetColumn}`, 'DESC'); } else if (sinceDate) { - q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: this.idService.gen(sinceDate) }); - q.orderBy(`${q.alias}.id`, 'ASC'); + q.andWhere(`${q.alias}.${targetColumn} > :sinceId`, { sinceId: this.idService.gen(sinceDate) }); + q.orderBy(`${q.alias}.${targetColumn}`, 'ASC'); } else if (untilDate) { - q.andWhere(`${q.alias}.id < :untilId`, { untilId: this.idService.gen(untilDate) }); - q.orderBy(`${q.alias}.id`, 'DESC'); + q.andWhere(`${q.alias}.${targetColumn} < :untilId`, { untilId: this.idService.gen(untilDate) }); + q.orderBy(`${q.alias}.${targetColumn}`, 'DESC'); } else { - q.orderBy(`${q.alias}.id`, 'DESC'); + q.orderBy(`${q.alias}.${targetColumn}`, 'DESC'); } return q; } diff --git a/packages/backend/src/server/api/endpoints/channels/followed.ts b/packages/backend/src/server/api/endpoints/channels/followed.ts index d2f36f251e..294b5e4bc4 100644 --- a/packages/backend/src/server/api/endpoints/channels/followed.ts +++ b/packages/backend/src/server/api/endpoints/channels/followed.ts @@ -48,7 +48,15 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.channelFollowingsRepository.createQueryBuilder(), ps.sinceId, ps.untilId) + const query = this.queryService + .makePaginationQuery( + this.channelFollowingsRepository.createQueryBuilder(), + ps.sinceId, + ps.untilId, + null, + null, + 'followeeId', + ) .andWhere({ followerId: me.id }); const followings = await query From 526057cc61a8a031a4689ddd742849547f6a0230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?= <46447427+samunohito@users.noreply.github.com> Date: Sat, 3 May 2025 16:23:06 +0900 Subject: [PATCH 3/5] =?UTF-8?q?Revert=20"fix:=20=E6=B7=BB=E4=BB=98?= =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=81=AE=E3=81=82=E3=82=8B?= =?UTF-8?q?=E3=83=AA=E3=82=AF=E3=82=A8=E3=82=B9=E3=83=88=E3=82=92=E5=8F=97?= =?UTF-8?q?=E3=81=91=E3=81=9F=E3=81=A8=E3=81=8D=E3=81=AE=E5=88=9D=E5=8B=95?= =?UTF-8?q?=E3=82=92=E6=94=B9=E5=96=84=20(#15896)"=20(#15927)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revert "fix: 添付ファイルのあるリクエストを受けたときの初動を改善 (#15896)" This reverts commit 7e8cc4d7c0a86ad0bf71a727fb16132e8bc180a8. * fix CHANGELOG.md --- CHANGELOG.md | 1 + packages/backend/package.json | 4 +- packages/backend/src/server/ServerService.ts | 7 +- .../backend/src/server/api/ApiCallService.ts | 166 ++++++------------ .../backend/src/server/api/endpoint-base.ts | 10 +- packages/backend/test/e2e/api.ts | 4 +- .../unit/server/api/drive/files/create.ts | 108 ------------ pnpm-lock.yaml | 158 ++++------------- 8 files changed, 96 insertions(+), 362 deletions(-) delete mode 100644 packages/backend/test/unit/server/api/drive/files/create.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index dcae039ae6..d56c2b2360 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - ただし、このオプションを有効にする場合、インデックスの作成にかかる時間が倍~3倍以上になることがあります。 - また、大きなインスタンスである場合にはインデックスの作成に失敗し、複数回再試行する必要がある可能性があります。 - Fix: チャンネルのフォロー一覧の結果が一部正しくないのを修正 (#12175) +- Fix: #15895 の取り消し ## 2025.4.1 diff --git a/packages/backend/package.json b/packages/backend/package.json index 3c6dcc6523..0574403ce1 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -223,7 +223,6 @@ "@types/semver": "7.7.0", "@types/simple-oauth2": "5.0.7", "@types/sinonjs__fake-timers": "8.1.5", - "@types/supertest": "6.0.3", "@types/tinycolor2": "1.4.6", "@types/tmp": "0.2.6", "@types/vary": "1.1.3", @@ -240,7 +239,6 @@ "jest-mock": "29.7.0", "nodemon": "3.1.10", "pid-port": "1.0.2", - "simple-oauth2": "5.1.0", - "supertest": "7.1.0" + "simple-oauth2": "5.1.0" } } diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index 7decdd2c10..355d7ca08e 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -73,7 +73,7 @@ export class ServerService implements OnApplicationShutdown { } @bindThis - public async launch() { + public async launch(): Promise { const fastify = Fastify({ trustProxy: true, logger: false, @@ -133,8 +133,8 @@ export class ServerService implements OnApplicationShutdown { reply.header('content-type', 'text/plain; charset=utf-8'); reply.header('link', `<${encodeURI(location)}>; rel="canonical"`); done(null, [ - 'Refusing to relay remote ActivityPub object lookup.', - '', + "Refusing to relay remote ActivityPub object lookup.", + "", `Please remove 'application/activity+json' and 'application/ld+json' from the Accept header or fetch using the authoritative URL at ${location}.`, ].join('\n')); }); @@ -301,7 +301,6 @@ export class ServerService implements OnApplicationShutdown { } await fastify.ready(); - return fastify; } @bindThis diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index 960c7b5476..a42fdaf730 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -6,11 +6,8 @@ import { randomUUID } from 'node:crypto'; import * as fs from 'node:fs'; import * as stream from 'node:stream/promises'; -import { Transform } from 'node:stream'; -import { type MultipartFile } from '@fastify/multipart'; import { Inject, Injectable } from '@nestjs/common'; import * as Sentry from '@sentry/node'; -import { AttachmentFile } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { getIpHash } from '@/misc/get-ip-hash.js'; import type { MiLocalUser, MiUser } from '@/models/User.js'; @@ -19,7 +16,7 @@ import type Logger from '@/logger.js'; import type { MiMeta, UserIpsRepository } from '@/models/_.js'; import { createTemp } from '@/misc/create-temp.js'; import { bindThis } from '@/decorators.js'; -import { type RolePolicies, RoleService } from '@/core/RoleService.js'; +import { RoleService } from '@/core/RoleService.js'; import type { Config } from '@/config.js'; import { ApiError } from './error.js'; import { RateLimiterService } from './RateLimiterService.js'; @@ -203,6 +200,18 @@ export class ApiCallService implements OnApplicationShutdown { return; } + const [path, cleanup] = await createTemp(); + await stream.pipeline(multipartData.file, fs.createWriteStream(path)); + + // ファイルサイズが制限を超えていた場合 + // なお truncated はストリームを読み切ってからでないと機能しないため、stream.pipeline より後にある必要がある + if (multipartData.file.truncated) { + cleanup(); + reply.code(413); + reply.send(); + return; + } + const fields = {} as Record; for (const [k, v] of Object.entries(multipartData.fields)) { fields[k] = typeof v === 'object' && 'value' in v ? v.value : undefined; @@ -217,7 +226,10 @@ export class ApiCallService implements OnApplicationShutdown { return; } this.authenticateService.authenticate(token).then(([user, app]) => { - this.call(endpoint, user, app, fields, multipartData, request).then((res) => { + this.call(endpoint, user, app, fields, { + name: multipartData.filename, + path: path, + }, request).then((res) => { this.send(reply, res); }).catch((err: ApiError) => { this.#sendApiError(reply, err); @@ -282,7 +294,10 @@ export class ApiCallService implements OnApplicationShutdown { user: MiLocalUser | null | undefined, token: MiAccessToken | null | undefined, data: any, - multipartFile: MultipartFile | null, + file: { + name: string; + path: string; + } | null, request: FastifyRequest<{ Body: Record | undefined, Querystring: Record }>, ) { const isSecure = user != null && token == null; @@ -356,37 +371,6 @@ export class ApiCallService implements OnApplicationShutdown { } } - // Cast non JSON input - if ((ep.meta.requireFile || request.method === 'GET') && ep.params.properties) { - for (const k of Object.keys(ep.params.properties)) { - const param = ep.params.properties![k]; - if (['boolean', 'number', 'integer'].includes(param.type ?? '') && typeof data[k] === 'string') { - try { - data[k] = JSON.parse(data[k]); - } catch (e) { - throw new ApiError({ - message: 'Invalid param.', - code: 'INVALID_PARAM', - id: '0b5f1631-7c1a-41a6-b399-cce335f34d85', - }, { - param: k, - reason: `cannot cast to ${param.type}`, - }); - } - } - } - } - - if (token && ((ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) - || (!ep.meta.kind && (ep.meta.requireCredential || ep.meta.requireModerator || ep.meta.requireAdmin)))) { - throw new ApiError({ - message: 'Your app does not have the necessary permissions to use this endpoint.', - code: 'PERMISSION_DENIED', - kind: 'permission', - id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838', - }); - } - if ((ep.meta.requireModerator || ep.meta.requireAdmin) && (this.meta.rootUserId !== user!.id)) { const myRoles = await this.roleService.getUserRoles(user!.id); if (ep.meta.requireModerator && !myRoles.some(r => r.isModerator || r.isAdministrator)) { @@ -420,91 +404,49 @@ export class ApiCallService implements OnApplicationShutdown { } } - let attachmentFile: AttachmentFile | null = null; - let cleanup = () => {}; - if (ep.meta.requireFile && request.method === 'POST' && multipartFile) { - const policies = await this.roleService.getUserPolicies(user!.id); - const result = await this.handleAttachmentFile( - Math.min((policies.maxFileSizeMb * 1024 * 1024), this.config.maxFileSize), - multipartFile, - ); - attachmentFile = result.attachmentFile; - cleanup = result.cleanup; + if (token && ((ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) + || (!ep.meta.kind && (ep.meta.requireCredential || ep.meta.requireModerator || ep.meta.requireAdmin)))) { + throw new ApiError({ + message: 'Your app does not have the necessary permissions to use this endpoint.', + code: 'PERMISSION_DENIED', + kind: 'permission', + id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838', + }); + } + + // Cast non JSON input + if ((ep.meta.requireFile || request.method === 'GET') && ep.params.properties) { + for (const k of Object.keys(ep.params.properties)) { + const param = ep.params.properties![k]; + if (['boolean', 'number', 'integer'].includes(param.type ?? '') && typeof data[k] === 'string') { + try { + data[k] = JSON.parse(data[k]); + } catch (e) { + throw new ApiError({ + message: 'Invalid param.', + code: 'INVALID_PARAM', + id: '0b5f1631-7c1a-41a6-b399-cce335f34d85', + }, { + param: k, + reason: `cannot cast to ${param.type}`, + }); + } + } + } } // API invoking if (this.config.sentryForBackend) { return await Sentry.startSpan({ name: 'API: ' + ep.name, - }, () => { - return ep.exec(data, user, token, attachmentFile, request.ip, request.headers) - .catch((err: Error) => this.#onExecError(ep, data, err, user?.id)) - .finally(() => cleanup()); - }); + }, () => ep.exec(data, user, token, file, request.ip, request.headers) + .catch((err: Error) => this.#onExecError(ep, data, err, user?.id))); } else { - return await ep.exec(data, user, token, attachmentFile, request.ip, request.headers) - .catch((err: Error) => this.#onExecError(ep, data, err, user?.id)) - .finally(() => cleanup()); + return await ep.exec(data, user, token, file, request.ip, request.headers) + .catch((err: Error) => this.#onExecError(ep, data, err, user?.id)); } } - @bindThis - private async handleAttachmentFile( - fileSizeLimit: number, - multipartFile: MultipartFile, - ) { - function createTooLongError() { - return new ApiError({ - httpStatusCode: 413, - kind: 'client', - message: 'File size is too large.', - code: 'FILE_SIZE_TOO_LARGE', - id: 'ff827ce8-9b4b-4808-8511-422222a3362f', - }); - } - - function createLimitStream(limit: number) { - let total = 0; - - return new Transform({ - transform(chunk, _, callback) { - total += chunk.length; - if (total > limit) { - callback(createTooLongError()); - } else { - callback(null, chunk); - } - }, - }); - } - - const [path, cleanup] = await createTemp(); - try { - await stream.pipeline( - multipartFile.file, - createLimitStream(fileSizeLimit), - fs.createWriteStream(path), - ); - - // ファイルサイズが制限を超えていた場合 - // なお truncated はストリームを読み切ってからでないと機能しないため、stream.pipeline より後にある必要がある - if (multipartFile.file.truncated) { - throw createTooLongError(); - } - } catch (err) { - cleanup(); - throw err; - } - - return { - attachmentFile: { - name: multipartFile.filename, - path, - }, - cleanup, - }; - } - @bindThis public dispose(): void { clearInterval(this.userIpHistoriesClearIntervalId); diff --git a/packages/backend/src/server/api/endpoint-base.ts b/packages/backend/src/server/api/endpoint-base.ts index b063487305..e061aa3a8e 100644 --- a/packages/backend/src/server/api/endpoint-base.ts +++ b/packages/backend/src/server/api/endpoint-base.ts @@ -21,23 +21,23 @@ ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/); export type Response = Record | void; -export type AttachmentFile = { +type File = { name: string | null; path: string; }; // TODO: paramsの型をT['params']のスキーマ定義から推論する type Executor = - (params: SchemaType, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: AttachmentFile, cleanup?: () => any, ip?: string | null, headers?: Record | null) => - Promise>>; + (params: SchemaType, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: File, cleanup?: () => any, ip?: string | null, headers?: Record | null) => + Promise>>; export abstract class Endpoint { - public exec: (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: AttachmentFile, ip?: string | null, headers?: Record | null) => Promise; + public exec: (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: File, ip?: string | null, headers?: Record | null) => Promise; constructor(meta: T, paramDef: Ps, cb: Executor) { const validate = ajv.compile(paramDef); - this.exec = (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: AttachmentFile, ip?: string | null, headers?: Record | null) => { + this.exec = (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: File, ip?: string | null, headers?: Record | null) => { let cleanup: undefined | (() => void) = undefined; if (meta.requireFile) { diff --git a/packages/backend/test/e2e/api.ts b/packages/backend/test/e2e/api.ts index f9e65aaa84..49c6a0636b 100644 --- a/packages/backend/test/e2e/api.ts +++ b/packages/backend/test/e2e/api.ts @@ -159,8 +159,8 @@ describe('API', () => { user: { token: application3 }, }, { status: 403, - code: 'PERMISSION_DENIED', - id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838', + code: 'ROLE_PERMISSION_DENIED', + id: 'c3d38592-54c0-429d-be96-5636b0431a61', }); await failedApiCall({ diff --git a/packages/backend/test/unit/server/api/drive/files/create.ts b/packages/backend/test/unit/server/api/drive/files/create.ts deleted file mode 100644 index b98892fa03..0000000000 --- a/packages/backend/test/unit/server/api/drive/files/create.ts +++ /dev/null @@ -1,108 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { S3Client } from '@aws-sdk/client-s3'; -import { Test, TestingModule } from '@nestjs/testing'; -import { mockClient } from 'aws-sdk-client-mock'; -import { FastifyInstance } from 'fastify'; -import request from 'supertest'; -import { CoreModule } from '@/core/CoreModule.js'; -import { RoleService } from '@/core/RoleService.js'; -import { DI } from '@/di-symbols.js'; -import { GlobalModule } from '@/GlobalModule.js'; -import { MiRole, UserProfilesRepository, UsersRepository } from '@/models/_.js'; -import { MiUser } from '@/models/User.js'; -import { ServerModule } from '@/server/ServerModule.js'; -import { ServerService } from '@/server/ServerService.js'; - -describe('/drive/files/create', () => { - let module: TestingModule; - let server: FastifyInstance; - const s3Mock = mockClient(S3Client); - let roleService: RoleService; - - let root: MiUser; - let role_tinyAttachment: MiRole; - - beforeAll(async () => { - module = await Test.createTestingModule({ - imports: [GlobalModule, CoreModule, ServerModule], - }).compile(); - module.enableShutdownHooks(); - - const serverService = module.get(ServerService); - server = await serverService.launch(); - - const usersRepository = module.get(DI.usersRepository); - root = await usersRepository.insert({ - id: 'root', - username: 'root', - usernameLower: 'root', - token: '1234567890123456', - }).then(x => usersRepository.findOneByOrFail(x.identifiers[0])); - - const userProfilesRepository = module.get(DI.userProfilesRepository); - await userProfilesRepository.insert({ - userId: root.id, - }); - - roleService = module.get(RoleService); - role_tinyAttachment = await roleService.create({ - name: 'test-role001', - description: 'Test role001 description', - target: 'manual', - policies: { - maxFileSizeMb: { - useDefault: false, - priority: 1, - // 10byte - value: 10 / 1024 / 1024, - }, - }, - }); - }); - - beforeEach(async () => { - s3Mock.reset(); - await roleService.unassign(root.id, role_tinyAttachment.id).catch(() => {}); - }); - - afterAll(async () => { - await server.close(); - await module.close(); - }); - - test('200 ok', async () => { - const result = await request(server.server) - .post('/api/drive/files/create') - .set('Content-Type', 'multipart/form-data') - .set('Authorization', `Bearer ${root.token}`) - .attach('file', Buffer.from('a'.repeat(1024 * 1024))); - expect(result.statusCode).toBe(200); - }); - - test('200 ok(with role)', async () => { - await roleService.assign(root.id, role_tinyAttachment.id); - - const result = await request(server.server) - .post('/api/drive/files/create') - .set('Content-Type', 'multipart/form-data') - .set('Authorization', `Bearer ${root.token}`) - .attach('file', Buffer.from('a'.repeat(10))); - expect(result.statusCode).toBe(200); - }); - - test('413 too large', async () => { - await roleService.assign(root.id, role_tinyAttachment.id); - - const result = await request(server.server) - .post('/api/drive/files/create') - .set('Content-Type', 'multipart/form-data') - .set('Authorization', `Bearer ${root.token}`) - .attach('file', Buffer.from('a'.repeat(11))); - expect(result.statusCode).toBe(413); - expect(result.body.error.code).toBe('FILE_SIZE_TOO_LARGE'); - }); -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b21d85a28..5d8bed52a1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -550,9 +550,6 @@ importers: '@types/sinonjs__fake-timers': specifier: 8.1.5 version: 8.1.5 - '@types/supertest': - specifier: 6.0.3 - version: 6.0.3 '@types/tinycolor2': specifier: 1.4.6 version: 1.4.6 @@ -604,9 +601,6 @@ importers: simple-oauth2: specifier: 5.1.0 version: 5.1.0 - supertest: - specifier: 7.1.0 - version: 7.1.0 optionalDependencies: '@swc/core-android-arm64': specifier: 1.3.11 @@ -3148,9 +3142,6 @@ packages: peerDependencies: '@opentelemetry/api': ^1.1.0 - '@paralleldrive/cuid2@2.2.2': - resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==} - '@parcel/watcher-android-arm64@2.5.0': resolution: {integrity: sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==} engines: {node: '>= 10.0.0'} @@ -4294,9 +4285,6 @@ packages: '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} - '@types/cookiejar@2.1.5': - resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} - '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -4384,9 +4372,6 @@ packages: '@types/mdx@2.0.3': resolution: {integrity: sha512-IgHxcT3RC8LzFLhKwP3gbMPeaK7BM9eBH46OdapPA7yvuIUJ8H6zHZV53J8hGZcTSnt95jANt+rTBNUUc22ACQ==} - '@types/methods@1.1.4': - resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} - '@types/micromatch@4.0.9': resolution: {integrity: sha512-7V+8ncr22h4UoYRLnLXSpTxjQrNUXtWHGeMPRJt1nULXI57G9bIcpyrHlmrQ7QK24EyyuXvYcSSWAM8GA9nqCg==} @@ -4525,12 +4510,6 @@ packages: '@types/statuses@2.0.4': resolution: {integrity: sha512-eqNDvZsCNY49OAXB0Firg/Sc2BgoWsntsLUdybGFOhAfCD6QJ2n9HXUIHGqt5qjrxmMv4wS8WLAw43ZkKcJ8Pw==} - '@types/superagent@8.1.9': - resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} - - '@types/supertest@6.0.3': - resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==} - '@types/tedious@4.0.14': resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} @@ -5596,9 +5575,6 @@ packages: compare-versions@6.1.1: resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} - component-emitter@1.3.1: - resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} - compress-commons@6.0.2: resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} engines: {node: '>= 14'} @@ -5656,9 +5632,6 @@ packages: resolution: {integrity: sha512-Xd8lFX4LM9QEEwxQpF9J9NTUh8pmdJO0cyRJhFiDoLTk2eH8FXlRv2IFGYVadZpqI3j8fhNrSdKCeYPxiAhLXw==} engines: {node: '>=18'} - cookiejar@2.1.4: - resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} - core-js@3.29.1: resolution: {integrity: sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw==} @@ -5968,9 +5941,6 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} - dezalgo@1.0.4: - resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} - diff-match-patch@1.0.5: resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} @@ -6632,10 +6602,6 @@ packages: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} - formidable@3.5.4: - resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==} - engines: {node: '>=14.0.0'} - forwarded-parse@2.1.2: resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} @@ -10049,14 +10015,6 @@ packages: peerDependencies: postcss: ^8.4.31 - superagent@9.0.2: - resolution: {integrity: sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==} - engines: {node: '>=14.18.0'} - - supertest@7.1.0: - resolution: {integrity: sha512-5QeSO8hSrKghtcWEoPiO036fxH0Ii2wVQfFZSP0oqQhmjk8bOLhDFXr4JrvaFmPuEWUoq4znY3uSi8UzLKxGqw==} - engines: {node: '>=14.18.0'} - supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -11570,7 +11528,7 @@ snapshots: '@babel/traverse': 7.24.7 '@babel/types': 7.25.6 convert-source-map: 2.0.0 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -11590,7 +11548,7 @@ snapshots: '@babel/traverse': 7.24.7 '@babel/types': 7.25.6 convert-source-map: 2.0.0 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -11812,7 +11770,7 @@ snapshots: '@babel/helper-split-export-declaration': 7.24.7 '@babel/parser': 7.25.6 '@babel/types': 7.25.6 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -12148,7 +12106,7 @@ snapshots: '@eslint/config-array@0.20.0': dependencies: '@eslint/object-schema': 2.1.6 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -12162,7 +12120,7 @@ snapshots: '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) espree: 10.3.0 globals: 14.0.0 ignore: 5.3.1 @@ -13201,10 +13159,6 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@paralleldrive/cuid2@2.2.2': - dependencies: - '@noble/hashes': 1.7.1 - '@parcel/watcher-android-arm64@2.5.0': optional: true @@ -14552,7 +14506,7 @@ snapshots: '@tokenizer/inflate@0.2.7': dependencies: - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) fflate: 0.8.2 token-types: 6.0.0 transitivePeerDependencies: @@ -14632,8 +14586,6 @@ snapshots: '@types/cookie@0.6.0': {} - '@types/cookiejar@2.1.5': {} - '@types/debug@4.1.12': dependencies: '@types/ms': 0.7.34 @@ -14727,8 +14679,6 @@ snapshots: '@types/mdx@2.0.3': {} - '@types/methods@1.1.4': {} - '@types/micromatch@4.0.9': dependencies: '@types/braces': 3.0.1 @@ -14864,18 +14814,6 @@ snapshots: '@types/statuses@2.0.4': {} - '@types/superagent@8.1.9': - dependencies: - '@types/cookiejar': 2.1.5 - '@types/methods': 1.1.4 - '@types/node': 22.15.2 - form-data: 4.0.2 - - '@types/supertest@6.0.3': - dependencies: - '@types/methods': 1.1.4 - '@types/superagent': 8.1.9 - '@types/tedious@4.0.14': dependencies: '@types/node': 22.15.2 @@ -14940,7 +14878,7 @@ snapshots: '@typescript-eslint/types': 8.31.0 '@typescript-eslint/typescript-estree': 8.31.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.31.0 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) eslint: 9.25.1 typescript: 5.8.3 transitivePeerDependencies: @@ -14955,7 +14893,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.31.0(typescript@5.8.3) '@typescript-eslint/utils': 8.31.0(eslint@9.25.1)(typescript@5.8.3) - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) eslint: 9.25.1 ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 @@ -14968,7 +14906,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.31.0 '@typescript-eslint/visitor-keys': 8.31.0 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -15005,7 +14943,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.6 @@ -15316,14 +15254,14 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) transitivePeerDependencies: - supports-color optional: true agent-base@7.1.0: dependencies: - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -16160,8 +16098,6 @@ snapshots: compare-versions@6.1.1: {} - component-emitter@1.3.1: {} - compress-commons@6.0.2: dependencies: crc-32: 1.2.2 @@ -16214,8 +16150,6 @@ snapshots: cookie@1.0.1: {} - cookiejar@2.1.4: {} - core-js@3.29.1: {} core-util-is@1.0.2: {} @@ -16598,11 +16532,6 @@ snapshots: dependencies: dequal: 2.0.3 - dezalgo@1.0.4: - dependencies: - asap: 2.0.6 - wrappy: 1.0.2 - diff-match-patch@1.0.5: {} diff-sequences@29.6.3: {} @@ -16905,7 +16834,7 @@ snapshots: esbuild-register@3.5.0(esbuild@0.25.3): dependencies: - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) esbuild: 0.25.3 transitivePeerDependencies: - supports-color @@ -17082,7 +17011,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) escape-string-regexp: 4.0.0 eslint-scope: 8.3.0 eslint-visitor-keys: 4.2.0 @@ -17533,7 +17462,7 @@ snapshots: follow-redirects@1.15.9(debug@4.4.0): optionalDependencies: - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) for-each@0.3.3: dependencies: @@ -17561,12 +17490,6 @@ snapshots: dependencies: fetch-blob: 3.2.0 - formidable@3.5.4: - dependencies: - '@paralleldrive/cuid2': 2.2.2 - dezalgo: 1.0.4 - once: 1.4.0 - forwarded-parse@2.1.2: {} forwarded@0.2.0: {} @@ -17981,7 +17904,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.3 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -18009,7 +17932,7 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) transitivePeerDependencies: - supports-color optional: true @@ -18017,14 +17940,14 @@ snapshots: https-proxy-agent@7.0.2: dependencies: agent-base: 7.1.0 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) transitivePeerDependencies: - supports-color https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.3 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -18122,7 +18045,7 @@ snapshots: dependencies: '@ioredis/commands': 1.2.0 cluster-key-slot: 1.1.2 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -18356,7 +18279,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -18365,7 +18288,7 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.25 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -19389,7 +19312,7 @@ snapshots: micromark@4.0.0: dependencies: '@types/debug': 4.1.12 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) decode-named-character-reference: 1.0.2 devlop: 1.1.0 micromark-core-commonmark: 2.0.0 @@ -20794,7 +20717,7 @@ snapshots: require-in-the-middle@7.3.0: dependencies: - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) module-details-from-path: 1.0.3 resolve: 1.22.8 transitivePeerDependencies: @@ -21244,7 +21167,7 @@ snapshots: socks-proxy-agent@8.0.2: dependencies: agent-base: 7.1.3 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) socks: 2.7.1 transitivePeerDependencies: - supports-color @@ -21353,7 +21276,7 @@ snapshots: arg: 5.0.2 bluebird: 3.7.2 check-more-types: 2.24.0 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) execa: 5.1.1 lazy-ass: 1.6.0 ps-tree: 1.2.0 @@ -21545,27 +21468,6 @@ snapshots: postcss: 8.5.3 postcss-selector-parser: 6.1.2 - superagent@9.0.2: - dependencies: - component-emitter: 1.3.1 - cookiejar: 2.1.4 - debug: 4.4.0(supports-color@8.1.1) - fast-safe-stringify: 2.1.1 - form-data: 4.0.2 - formidable: 3.5.4 - methods: 1.1.2 - mime: 2.6.0 - qs: 6.14.0 - transitivePeerDependencies: - - supports-color - - supertest@7.1.0: - dependencies: - methods: 1.1.2 - superagent: 9.0.2 - transitivePeerDependencies: - - supports-color - supports-color@5.5.0: dependencies: has-flag: 3.0.0 @@ -21923,7 +21825,7 @@ snapshots: app-root-path: 3.1.0 buffer: 6.0.3 dayjs: 1.11.13 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) dotenv: 16.4.7 glob: 10.4.5 reflect-metadata: 0.2.2 @@ -22117,7 +22019,7 @@ snapshots: vite-node@3.1.2(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3): dependencies: cac: 6.7.14 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) es-module-lexer: 1.6.0 pathe: 2.0.3 vite: 6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3) @@ -22166,7 +22068,7 @@ snapshots: '@vitest/spy': 3.1.2 '@vitest/utils': 3.1.2 chai: 5.2.0 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) expect-type: 1.2.1 magic-string: 0.30.17 pathe: 2.0.3 @@ -22253,7 +22155,7 @@ snapshots: vue-eslint-parser@10.1.3(eslint@9.25.1): dependencies: - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) eslint: 9.25.1 eslint-scope: 8.3.0 eslint-visitor-keys: 4.2.0 From 00008d37630613d487e35c25063d88926d8c0a95 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sat, 3 May 2025 16:25:09 +0900 Subject: [PATCH 4/5] Update CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d56c2b2360..a54b613c82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,8 @@ - ただし、このオプションを有効にする場合、インデックスの作成にかかる時間が倍~3倍以上になることがあります。 - また、大きなインスタンスである場合にはインデックスの作成に失敗し、複数回再試行する必要がある可能性があります。 - Fix: チャンネルのフォロー一覧の結果が一部正しくないのを修正 (#12175) -- Fix: #15895 の取り消し +- Fix: ファイルをアップロードした際にファイル名が常に untitled になる問題を修正 +- Fix: ファイルのアップロードに失敗することがある問題を修正 ## 2025.4.1 From f0544ede879361188733166f328c80a45afa5f0e Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sat, 3 May 2025 16:51:23 +0900 Subject: [PATCH 5/5] tweak MkPullToRefresh --- packages/frontend/src/components/MkPullToRefresh.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue index ad828c50b6..4910f34a1f 100644 --- a/packages/frontend/src/components/MkPullToRefresh.vue +++ b/packages/frontend/src/components/MkPullToRefresh.vue @@ -29,7 +29,7 @@ import { isHorizontalSwipeSwiping } from '@/utility/touch.js'; const SCROLL_STOP = 10; const MAX_PULL_DISTANCE = Infinity; -const FIRE_THRESHOLD = 230; +const FIRE_THRESHOLD = 200; const RELEASE_TRANSITION_DURATION = 200; const PULL_BRAKE_BASE = 1.5; const PULL_BRAKE_FACTOR = 170; @@ -239,7 +239,6 @@ onUnmounted(() => { display: flex; flex-direction: column; align-items: center; - font-size: 14px; > .icon, > .loader { margin: 6px 0; @@ -255,6 +254,7 @@ onUnmounted(() => { > .text { margin: 5px 0; + font-size: 90%; } }