spec(media-proxy): urlをクエリではなくパラメータで指定するように (MisskeyIO#922)
This commit is contained in:
parent
ff85d650bf
commit
6462968b9d
5 changed files with 116 additions and 67 deletions
|
@ -77,9 +77,8 @@ export class DriveFileEntityService {
|
||||||
@bindThis
|
@bindThis
|
||||||
private getProxiedUrl(url: string, mode?: 'static' | 'avatar'): string {
|
private getProxiedUrl(url: string, mode?: 'static' | 'avatar'): string {
|
||||||
return appendQuery(
|
return appendQuery(
|
||||||
`${this.config.mediaProxy}/${mode ?? 'image'}.webp`,
|
`${this.config.mediaProxy}/${mode ?? 'image'}/${encodeURIComponent(url)}`,
|
||||||
query({
|
query({
|
||||||
url,
|
|
||||||
...(mode ? { [mode]: '1' } : {}),
|
...(mode ? { [mode]: '1' } : {}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
@ -26,6 +26,7 @@ import { FileInfoService } from '@/core/FileInfoService.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { isMimeImage } from '@/misc/is-mime-image.js';
|
import { isMimeImage } from '@/misc/is-mime-image.js';
|
||||||
|
import { appendQuery, query } from '@/misc/prelude/url.js';
|
||||||
import { correctFilename } from '@/misc/correct-filename.js';
|
import { correctFilename } from '@/misc/correct-filename.js';
|
||||||
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
|
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
|
||||||
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
|
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
|
||||||
|
@ -35,6 +36,16 @@ const _dirname = dirname(_filename);
|
||||||
|
|
||||||
const assets = `${_dirname}/../../server/file/assets/`;
|
const assets = `${_dirname}/../../server/file/assets/`;
|
||||||
|
|
||||||
|
interface TransformQuery {
|
||||||
|
origin?: string;
|
||||||
|
fallback?: string;
|
||||||
|
emoji?: string;
|
||||||
|
avatar?: string;
|
||||||
|
static?: string;
|
||||||
|
preview?: string;
|
||||||
|
badge?: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FileServerService {
|
export class FileServerService {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
@ -87,10 +98,18 @@ export class FileServerService {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fastify.get<{
|
||||||
|
Params: { type: string; url: string; };
|
||||||
|
Querystring: { url?: string; } & TransformQuery;
|
||||||
|
}>('/proxy/:type/:url', async (request, reply) => {
|
||||||
|
return await this.proxyHandler(request, reply)
|
||||||
|
.catch(err => this.errorHandler(request, reply, err));
|
||||||
|
});
|
||||||
|
|
||||||
fastify.get<{
|
fastify.get<{
|
||||||
Params: { url: string; };
|
Params: { url: string; };
|
||||||
Querystring: { url?: string; };
|
Querystring: { url?: string; } & TransformQuery;
|
||||||
}>('/proxy/:url*', async (request, reply) => {
|
}>('/proxy/:url', async (request, reply) => {
|
||||||
return await this.proxyHandler(request, reply)
|
return await this.proxyHandler(request, reply)
|
||||||
.catch(err => this.errorHandler(request, reply, err));
|
.catch(err => this.errorHandler(request, reply, err));
|
||||||
});
|
});
|
||||||
|
@ -142,12 +161,15 @@ export class FileServerService {
|
||||||
if (isMimeImage(file.mime, 'sharp-convertible-image-with-bmp')) {
|
if (isMimeImage(file.mime, 'sharp-convertible-image-with-bmp')) {
|
||||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
||||||
|
|
||||||
const url = new URL(`${this.config.mediaProxy}/static.webp`);
|
const url = appendQuery(
|
||||||
url.searchParams.set('url', file.url);
|
`${this.config.mediaProxy}/static/${encodeURIComponent(file.url)}`,
|
||||||
url.searchParams.set('static', '1');
|
query({
|
||||||
|
static: '1',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
file.cleanup();
|
file.cleanup();
|
||||||
return await reply.redirect(url.toString(), 301);
|
return await reply.redirect(url, 301);
|
||||||
} else if (file.mime.startsWith('video/')) {
|
} else if (file.mime.startsWith('video/')) {
|
||||||
const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url);
|
const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url);
|
||||||
if (externalThumbnail) {
|
if (externalThumbnail) {
|
||||||
|
@ -163,11 +185,10 @@ export class FileServerService {
|
||||||
if (['image/svg+xml'].includes(file.mime)) {
|
if (['image/svg+xml'].includes(file.mime)) {
|
||||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
||||||
|
|
||||||
const url = new URL(`${this.config.mediaProxy}/svg.webp`);
|
const url = `${this.config.mediaProxy}/svg/${encodeURIComponent(file.url)}`;
|
||||||
url.searchParams.set('url', file.url);
|
|
||||||
|
|
||||||
file.cleanup();
|
file.cleanup();
|
||||||
return await reply.redirect(url.toString(), 301);
|
return await reply.redirect(url, 301);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -291,30 +312,43 @@ export class FileServerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async proxyHandler(request: FastifyRequest<{ Params: { url: string; }; Querystring: { url?: string; }; }>, reply: FastifyReply) {
|
private async proxyHandler(request: FastifyRequest<{ Params: { type?: string; url: string; }; Querystring: { url?: string; } & TransformQuery; }>, reply: FastifyReply) {
|
||||||
const url = 'url' in request.query ? request.query.url : 'https://' + request.params.url;
|
let url: string;
|
||||||
|
if ('url' in request.query && request.query.url) {
|
||||||
|
url = request.query.url;
|
||||||
|
} else {
|
||||||
|
url = request.params.url;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof url !== 'string') {
|
// noinspection HttpUrlsUsage
|
||||||
|
if (url
|
||||||
|
&& !url.startsWith('http://')
|
||||||
|
&& !url.startsWith('https://')
|
||||||
|
) {
|
||||||
|
url = 'https://' + url;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
reply.code(400);
|
reply.code(400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// アバタークロップなど、どうしてもオリジンである必要がある場合
|
// アバタークロップなど、どうしてもオリジンである必要がある場合
|
||||||
const mustOrigin = 'origin' in request.query;
|
const mustOrigin = 'origin' in request.query;
|
||||||
|
const transformQuery = request.query as TransformQuery;
|
||||||
|
|
||||||
if (this.config.externalMediaProxyEnabled && !mustOrigin) {
|
if (this.config.externalMediaProxyEnabled && !mustOrigin) {
|
||||||
// 外部のメディアプロキシが有効なら、そちらにリダイレクト
|
// 外部のメディアプロキシが有効なら、そちらにリダイレクト
|
||||||
|
|
||||||
reply.header('Cache-Control', 'public, max-age=259200'); // 3 days
|
reply.header('Cache-Control', 'public, max-age=259200'); // 3 days
|
||||||
|
|
||||||
const url = new URL(`${this.config.mediaProxy}/${request.params.url || ''}`);
|
const url = appendQuery(
|
||||||
|
`${this.config.mediaProxy}/redirect/${encodeURIComponent(request.params.url)}`,
|
||||||
for (const [key, value] of Object.entries(request.query)) {
|
query(transformQuery as Record<string, string>),
|
||||||
url.searchParams.append(key, value);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return reply.redirect(
|
return reply.redirect(
|
||||||
url.toString(),
|
url,
|
||||||
301,
|
301,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -344,11 +378,11 @@ export class FileServerService {
|
||||||
const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image-with-bmp');
|
const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image-with-bmp');
|
||||||
|
|
||||||
if (
|
if (
|
||||||
'emoji' in request.query ||
|
'emoji' in transformQuery ||
|
||||||
'avatar' in request.query ||
|
'avatar' in transformQuery ||
|
||||||
'static' in request.query ||
|
'static' in transformQuery ||
|
||||||
'preview' in request.query ||
|
'preview' in transformQuery ||
|
||||||
'badge' in request.query
|
'badge' in transformQuery
|
||||||
) {
|
) {
|
||||||
if (!isConvertibleImage) {
|
if (!isConvertibleImage) {
|
||||||
// 画像でないなら404でお茶を濁す
|
// 画像でないなら404でお茶を濁す
|
||||||
|
@ -357,17 +391,17 @@ export class FileServerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
let image: IImageStreamable | null = null;
|
let image: IImageStreamable | null = null;
|
||||||
if ('emoji' in request.query || 'avatar' in request.query) {
|
if ('emoji' in transformQuery || 'avatar' in transformQuery) {
|
||||||
if (!isAnimationConvertibleImage && !('static' in request.query)) {
|
if (!isAnimationConvertibleImage && !('static' in transformQuery)) {
|
||||||
image = {
|
image = {
|
||||||
data: fs.createReadStream(file.path),
|
data: fs.createReadStream(file.path),
|
||||||
ext: file.ext,
|
ext: file.ext,
|
||||||
type: file.mime,
|
type: file.mime,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const data = (await sharpBmp(file.path, file.mime, { animated: !('static' in request.query) }))
|
const data = (await sharpBmp(file.path, file.mime, { animated: !('static' in transformQuery) }))
|
||||||
.resize({
|
.resize({
|
||||||
height: 'emoji' in request.query ? 128 : 320,
|
height: 'emoji' in transformQuery ? 128 : 320,
|
||||||
withoutEnlargement: true,
|
withoutEnlargement: true,
|
||||||
})
|
})
|
||||||
.webp(webpDefault);
|
.webp(webpDefault);
|
||||||
|
@ -378,11 +412,11 @@ export class FileServerService {
|
||||||
type: 'image/webp',
|
type: 'image/webp',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else if ('static' in request.query) {
|
} else if ('static' in transformQuery) {
|
||||||
image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 498, 422);
|
image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 498, 422);
|
||||||
} else if ('preview' in request.query) {
|
} else if ('preview' in transformQuery) {
|
||||||
image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 200, 200);
|
image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 200, 200);
|
||||||
} else if ('badge' in request.query) {
|
} else if ('badge' in transformQuery) {
|
||||||
const mask = (await sharpBmp(file.path, file.mime))
|
const mask = (await sharpBmp(file.path, file.mime))
|
||||||
.resize(96, 96, {
|
.resize(96, 96, {
|
||||||
fit: 'contain',
|
fit: 'contain',
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { DI } from '@/di-symbols.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import * as Acct from '@/misc/acct.js';
|
import * as Acct from '@/misc/acct.js';
|
||||||
import { genIdenticon } from '@/misc/gen-identicon.js';
|
import { genIdenticon } from '@/misc/gen-identicon.js';
|
||||||
|
import { appendQuery, query } from '@/misc/prelude/url.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
@ -162,22 +163,28 @@ export class ServerService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let url: URL;
|
let url: string;
|
||||||
if ('badge' in request.query) {
|
if ('badge' in request.query) {
|
||||||
url = new URL(`${this.config.mediaProxy}/emoji.png`);
|
url = appendQuery(
|
||||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||||
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
|
`${this.config.mediaProxy}/emoji/${encodeURIComponent(emoji.publicUrl || emoji.originalUrl)}`,
|
||||||
url.searchParams.set('badge', '1');
|
query({
|
||||||
|
badge: '1',
|
||||||
|
}),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
url = new URL(`${this.config.mediaProxy}/emoji.webp`);
|
url = appendQuery(
|
||||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||||
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
|
`${this.config.mediaProxy}/emoji/${encodeURIComponent(emoji.publicUrl || emoji.originalUrl)}`,
|
||||||
url.searchParams.set('emoji', '1');
|
query({
|
||||||
if ('static' in request.query) url.searchParams.set('static', '1');
|
emoji: '1',
|
||||||
|
...('static' in request.query ? { static: '1' } : {}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return reply.redirect(
|
return reply.redirect(
|
||||||
url.toString(),
|
url,
|
||||||
301,
|
301,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,7 +12,7 @@ import type { Config } from '@/config.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { query } from '@/misc/prelude/url.js';
|
import { appendQuery, query } from '@/misc/prelude/url.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { ApiError } from '@/server/api/error.js';
|
import { ApiError } from '@/server/api/error.js';
|
||||||
|
@ -36,14 +36,15 @@ export class UrlPreviewService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private wrap(url?: string | null): string | null {
|
private wrap(url?: string | null): string | null {
|
||||||
return url != null
|
if (!url) return null;
|
||||||
? url.match(/^https?:\/\//)
|
if (!RegExp(/^https?:\/\//).exec(url)) return url;
|
||||||
? `${this.config.mediaProxy}/preview.webp?${query({
|
|
||||||
url,
|
return appendQuery(
|
||||||
preview: '1',
|
`${this.config.mediaProxy}/preview/${encodeURIComponent(url)}`,
|
||||||
})}`
|
query({
|
||||||
: url
|
preview: '1',
|
||||||
: null;
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { query } from '@/scripts/url.js';
|
import { appendQuery, query } from '@/scripts/url.js';
|
||||||
import { url } from '@/config.js';
|
import { url } from '@/config.js';
|
||||||
import { instance } from '@/instance.js';
|
import { instance } from '@/instance.js';
|
||||||
|
|
||||||
|
@ -12,18 +12,26 @@ export function getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji'
|
||||||
|
|
||||||
if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) {
|
if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) {
|
||||||
// もう既にproxyっぽそうだったらurlを取り出す
|
// もう既にproxyっぽそうだったらurlを取り出す
|
||||||
imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl;
|
const url = (new URL(imageUrl)).searchParams.get('url');
|
||||||
|
if (url) {
|
||||||
|
imageUrl = url;
|
||||||
|
} else if (imageUrl.startsWith(instance.mediaProxy + '/')) {
|
||||||
|
imageUrl = imageUrl.slice(instance.mediaProxy.length + 1);
|
||||||
|
} else if (imageUrl.startsWith('/proxy/')) {
|
||||||
|
imageUrl = imageUrl.slice('/proxy/'.length);
|
||||||
|
} else if (imageUrl.startsWith(localProxy + '/')) {
|
||||||
|
imageUrl = imageUrl.slice(localProxy.length + 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${mustOrigin ? localProxy : instance.mediaProxy}/${
|
return appendQuery(
|
||||||
type === 'preview' ? 'preview.webp'
|
`${mustOrigin ? localProxy : instance.mediaProxy}/${type === 'preview' ? 'preview' : 'image'}/${encodeURIComponent(imageUrl)}`,
|
||||||
: 'image.webp'
|
query({
|
||||||
}?${query({
|
...(!noFallback ? { 'fallback': '1' } : {}),
|
||||||
url: imageUrl,
|
...(type ? { [type]: '1' } : {}),
|
||||||
...(!noFallback ? { 'fallback': '1' } : {}),
|
...(mustOrigin ? { origin: '1' } : {}),
|
||||||
...(type ? { [type]: '1' } : {}),
|
}),
|
||||||
...(mustOrigin ? { origin: '1' } : {}),
|
);
|
||||||
})}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null {
|
export function getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null {
|
||||||
|
@ -46,8 +54,8 @@ export function getStaticImageUrl(baseUrl: string): string {
|
||||||
return u.href;
|
return u.href;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${instance.mediaProxy}/static.webp?${query({
|
return appendQuery(
|
||||||
url: u.href,
|
`${instance.mediaProxy}/static/${encodeURIComponent(u.href)}`,
|
||||||
static: '1',
|
query({ static: '1' }),
|
||||||
})}`;
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue