mirror of
https://github.com/misskey-dev/misskey.git
synced 2024-12-29 03:28:29 +01:00
リモートサーバーのファイルをデータベースに保存せず、クライアントで直接表示させられるように
This commit is contained in:
parent
e804757d83
commit
558b897700
10 changed files with 110 additions and 60 deletions
|
@ -8,7 +8,8 @@ const { default: User } = require('../built/models/user');
|
||||||
const q = {
|
const q = {
|
||||||
'metadata._user.host': {
|
'metadata._user.host': {
|
||||||
$ne: null
|
$ne: null
|
||||||
}
|
},
|
||||||
|
'metadata.isMetaOnly': false
|
||||||
};
|
};
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
@ -56,8 +57,7 @@ async function main() {
|
||||||
|
|
||||||
DriveFile.update({ _id: file._id }, {
|
DriveFile.update({ _id: file._id }, {
|
||||||
$set: {
|
$set: {
|
||||||
'metadata.deletedAt': new Date(),
|
'metadata.isMetaOnly': true
|
||||||
'metadata.isExpired': true
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
]).then(async () => {
|
]).then(async () => {
|
||||||
|
|
|
@ -855,6 +855,8 @@ mobile/views/pages/settings.vue:
|
||||||
behavior: "動作"
|
behavior: "動作"
|
||||||
fetch-on-scroll: "スクロールで自動読み込み"
|
fetch-on-scroll: "スクロールで自動読み込み"
|
||||||
disable-via-mobile: "「モバイルからの投稿」フラグを付けない"
|
disable-via-mobile: "「モバイルからの投稿」フラグを付けない"
|
||||||
|
load-raw-images: "添付された画像を高画質で表示する"
|
||||||
|
load-remote-media: "リモートサーバーのメディアを表示する"
|
||||||
twitter: "Twitter連携"
|
twitter: "Twitter連携"
|
||||||
twitter-connect: "Twitterアカウントに接続する"
|
twitter-connect: "Twitterアカウントに接続する"
|
||||||
twitter-reconnect: "再接続する"
|
twitter-reconnect: "再接続する"
|
||||||
|
|
|
@ -16,13 +16,18 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
lightmode(): boolean {
|
|
||||||
return this.$store.state.device.lightmode;
|
|
||||||
},
|
|
||||||
style(): any {
|
style(): any {
|
||||||
|
let url = `url(${this.image.url}?thumbnail)`;
|
||||||
|
|
||||||
|
if (this.$store.state.device.loadRemoteMedia || this.$store.state.device.lightmode) {
|
||||||
|
url = null;
|
||||||
|
} else if (this.raw || this.$store.state.device.loadRawImages) {
|
||||||
|
url = `url(${this.image.url})`;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'background-color': this.image.properties.avgColor && this.image.properties.avgColor.length == 3 ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
|
'background-color': this.image.properties.avgColor && this.image.properties.avgColor.length == 3 ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
|
||||||
'background-image': this.lightmode ? null : this.raw ? `url(${this.image.url})` : `url(${this.image.url}?thumbnail&size=512)`
|
'background-image': url
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,6 +59,14 @@
|
||||||
<md-switch v-model="clientSettings.disableViaMobile" @change="onChangeDisableViaMobile">%i18n:@disable-via-mobile%</md-switch>
|
<md-switch v-model="clientSettings.disableViaMobile" @change="onChangeDisableViaMobile">%i18n:@disable-via-mobile%</md-switch>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<md-switch v-model="loadRawImages">%i18n:@load-raw-images%</md-switch>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<md-switch v-model="clientSettings.loadRemoteMedia" @change="onChangeLoadRemoteMedia">%i18n:@load-remote-media%</md-switch>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<md-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</md-switch>
|
<md-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</md-switch>
|
||||||
</div>
|
</div>
|
||||||
|
@ -166,6 +174,11 @@ export default Vue.extend({
|
||||||
set(value) { this.$store.commit('device/set', { key: 'lightmode', value }); }
|
set(value) { this.$store.commit('device/set', { key: 'lightmode', value }); }
|
||||||
},
|
},
|
||||||
|
|
||||||
|
loadRawImages: {
|
||||||
|
get() { return this.$store.state.device.loadRawImages; },
|
||||||
|
set(value) { this.$store.commit('device/set', { key: 'loadRawImages', value }); }
|
||||||
|
},
|
||||||
|
|
||||||
lang: {
|
lang: {
|
||||||
get() { return this.$store.state.device.lang; },
|
get() { return this.$store.state.device.lang; },
|
||||||
set(value) { this.$store.commit('device/set', { key: 'lang', value }); }
|
set(value) { this.$store.commit('device/set', { key: 'lang', value }); }
|
||||||
|
@ -195,6 +208,13 @@ export default Vue.extend({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onChangeLoadRemoteMedia(v) {
|
||||||
|
this.$store.dispatch('settings/set', {
|
||||||
|
key: 'loadRemoteMedia',
|
||||||
|
value: v
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
onChangeCircleIcons(v) {
|
onChangeCircleIcons(v) {
|
||||||
this.$store.dispatch('settings/set', {
|
this.$store.dispatch('settings/set', {
|
||||||
key: 'circleIcons',
|
key: 'circleIcons',
|
||||||
|
|
|
@ -13,7 +13,9 @@ const defaultSettings = {
|
||||||
gradientWindowHeader: false,
|
gradientWindowHeader: false,
|
||||||
showReplyTarget: true,
|
showReplyTarget: true,
|
||||||
showMyRenotes: true,
|
showMyRenotes: true,
|
||||||
showRenotedMyNotes: true
|
showRenotedMyNotes: true,
|
||||||
|
loadRemoteMedia: true,
|
||||||
|
disableViaMobile: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultDeviceSettings = {
|
const defaultDeviceSettings = {
|
||||||
|
@ -26,6 +28,7 @@ const defaultDeviceSettings = {
|
||||||
preventUpdate: false,
|
preventUpdate: false,
|
||||||
debug: false,
|
debug: false,
|
||||||
lightmode: false,
|
lightmode: false,
|
||||||
|
loadRawImages: false,
|
||||||
postStyle: 'standard'
|
postStyle: 'standard'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,8 @@ export type Source = {
|
||||||
secret_key: string;
|
secret_key: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
preventCacheRemoteFiles: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ゴーストアカウントのID
|
* ゴーストアカウントのID
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -32,7 +32,7 @@ export type IMetadata = {
|
||||||
uri?: string;
|
uri?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
deletedAt?: Date;
|
deletedAt?: Date;
|
||||||
isExpired?: boolean;
|
isMetaOnly?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IDriveFile = {
|
export type IDriveFile = {
|
||||||
|
@ -155,7 +155,8 @@ export const pack = (
|
||||||
_target = Object.assign(_target, _file.metadata);
|
_target = Object.assign(_target, _file.metadata);
|
||||||
|
|
||||||
_target.src = _file.metadata.url;
|
_target.src = _file.metadata.url;
|
||||||
_target.url = `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`;
|
_target.url = _file.metadata.isMetaOnly ? _file.metadata.url : `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`;
|
||||||
|
_target.isRemote = _file.metadata.isMetaOnly;
|
||||||
|
|
||||||
if (_target.properties == null) _target.properties = {};
|
if (_target.properties == null) _target.properties = {};
|
||||||
|
|
||||||
|
|
|
@ -33,11 +33,12 @@ export default async function(ctx: Koa.Context) {
|
||||||
|
|
||||||
if (file.metadata.deletedAt) {
|
if (file.metadata.deletedAt) {
|
||||||
ctx.status = 410;
|
ctx.status = 410;
|
||||||
if (file.metadata.isExpired) {
|
await send(ctx, '/tombstone.png', { root: assets });
|
||||||
await send(ctx, '/cache-expired.png', { root: assets });
|
return;
|
||||||
} else {
|
}
|
||||||
await send(ctx, '/tombstone.png', { root: assets });
|
|
||||||
}
|
if (file.metadata.isMetaOnly) {
|
||||||
|
ctx.status = 204;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -104,6 +104,7 @@ export default async function(
|
||||||
comment: string = null,
|
comment: string = null,
|
||||||
folderId: mongodb.ObjectID = null,
|
folderId: mongodb.ObjectID = null,
|
||||||
force: boolean = false,
|
force: boolean = false,
|
||||||
|
metaOnly: boolean = false,
|
||||||
url: string = null,
|
url: string = null,
|
||||||
uri: string = null
|
uri: string = null
|
||||||
): Promise<IDriveFile> {
|
): Promise<IDriveFile> {
|
||||||
|
@ -170,38 +171,40 @@ export default async function(
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region Check drive usage
|
//#region Check drive usage
|
||||||
const usage = await DriveFile
|
if (!metaOnly) {
|
||||||
.aggregate([{
|
const usage = await DriveFile
|
||||||
$match: {
|
.aggregate([{
|
||||||
'metadata.userId': user._id,
|
$match: {
|
||||||
'metadata.deletedAt': { $exists: false }
|
'metadata.userId': user._id,
|
||||||
}
|
'metadata.deletedAt': { $exists: false }
|
||||||
}, {
|
}
|
||||||
$project: {
|
}, {
|
||||||
length: true
|
$project: {
|
||||||
}
|
length: true
|
||||||
}, {
|
}
|
||||||
$group: {
|
}, {
|
||||||
_id: null,
|
$group: {
|
||||||
usage: { $sum: '$length' }
|
_id: null,
|
||||||
}
|
usage: { $sum: '$length' }
|
||||||
}])
|
}
|
||||||
.then((aggregates: any[]) => {
|
}])
|
||||||
if (aggregates.length > 0) {
|
.then((aggregates: any[]) => {
|
||||||
return aggregates[0].usage;
|
if (aggregates.length > 0) {
|
||||||
}
|
return aggregates[0].usage;
|
||||||
return 0;
|
}
|
||||||
});
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
log(`drive usage is ${usage}`);
|
log(`drive usage is ${usage}`);
|
||||||
|
|
||||||
// If usage limit exceeded
|
// If usage limit exceeded
|
||||||
if (usage + size > user.driveCapacity) {
|
if (usage + size > user.driveCapacity) {
|
||||||
if (isLocalUser(user)) {
|
if (isLocalUser(user)) {
|
||||||
throw 'no-free-space';
|
throw 'no-free-space';
|
||||||
} else {
|
} else {
|
||||||
// (アバターまたはバナーを含まず)最も古いファイルを削除する
|
// (アバターまたはバナーを含まず)最も古いファイルを削除する
|
||||||
deleteOldFile(user);
|
deleteOldFile(user);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
@ -270,8 +273,6 @@ export default async function(
|
||||||
|
|
||||||
const [folder] = await Promise.all([fetchFolder(), propPromises]);
|
const [folder] = await Promise.all([fetchFolder(), propPromises]);
|
||||||
|
|
||||||
const readable = fs.createReadStream(path);
|
|
||||||
|
|
||||||
const metadata = {
|
const metadata = {
|
||||||
userId: user._id,
|
userId: user._id,
|
||||||
_user: {
|
_user: {
|
||||||
|
@ -279,7 +280,8 @@ export default async function(
|
||||||
},
|
},
|
||||||
folderId: folder !== null ? folder._id : null,
|
folderId: folder !== null ? folder._id : null,
|
||||||
comment: comment,
|
comment: comment,
|
||||||
properties: properties
|
properties: properties,
|
||||||
|
isMetaOnly: metaOnly
|
||||||
} as IMetadata;
|
} as IMetadata;
|
||||||
|
|
||||||
if (url !== null) {
|
if (url !== null) {
|
||||||
|
@ -290,7 +292,16 @@ export default async function(
|
||||||
metadata.uri = uri;
|
metadata.uri = uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
const driveFile = await (writeChunks(detectedName, readable, mime, metadata) as Promise<IDriveFile>);
|
const driveFile = metaOnly
|
||||||
|
? await DriveFile.insert({
|
||||||
|
length: 0,
|
||||||
|
uploadDate: new Date(),
|
||||||
|
md5: hash,
|
||||||
|
filename: detectedName,
|
||||||
|
metadata: metadata,
|
||||||
|
contentType: mime
|
||||||
|
})
|
||||||
|
: await (writeChunks(detectedName, fs.createReadStream(path), mime, metadata) as Promise<IDriveFile>);
|
||||||
|
|
||||||
log(`drive file has been created ${driveFile._id}`);
|
log(`drive file has been created ${driveFile._id}`);
|
||||||
|
|
||||||
|
@ -300,13 +311,15 @@ export default async function(
|
||||||
publishDriveStream(user._id, 'file_created', packedFile);
|
publishDriveStream(user._id, 'file_created', packedFile);
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
if (!metaOnly) {
|
||||||
const thumb = await genThumbnail(driveFile);
|
try {
|
||||||
if (thumb) {
|
const thumb = await genThumbnail(driveFile);
|
||||||
await writeThumbnailChunks(detectedName, thumb, driveFile._id);
|
if (thumb) {
|
||||||
|
await writeThumbnailChunks(detectedName, thumb, driveFile._id);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// noop
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
// noop
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return driveFile;
|
return driveFile;
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
|
import * as fs from 'fs';
|
||||||
import * as URL from 'url';
|
import * as URL from 'url';
|
||||||
import { IDriveFile, validateFileName } from '../../models/drive-file';
|
|
||||||
import create from './add-file';
|
|
||||||
import * as debug from 'debug';
|
import * as debug from 'debug';
|
||||||
import * as tmp from 'tmp';
|
import * as tmp from 'tmp';
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as request from 'request';
|
import * as request from 'request';
|
||||||
|
|
||||||
|
import { IDriveFile, validateFileName } from '../../models/drive-file';
|
||||||
|
import create from './add-file';
|
||||||
|
import config from '../../config';
|
||||||
|
|
||||||
const log = debug('misskey:drive:upload-from-url');
|
const log = debug('misskey:drive:upload-from-url');
|
||||||
|
|
||||||
export default async (url, user, folderId = null, uri = null): Promise<IDriveFile> => {
|
export default async (url: string, user, folderId = null, uri: string = null): Promise<IDriveFile> => {
|
||||||
log(`REQUESTED: ${url}`);
|
log(`REQUESTED: ${url}`);
|
||||||
|
|
||||||
let name = URL.parse(url).pathname.split('/').pop();
|
let name = URL.parse(url).pathname.split('/').pop();
|
||||||
|
@ -43,7 +46,7 @@ export default async (url, user, folderId = null, uri = null): Promise<IDriveFil
|
||||||
let error;
|
let error;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
driveFile = await create(user, path, name, null, folderId, false, url, uri);
|
driveFile = await create(user, path, name, null, folderId, false, config.preventCacheRemoteFiles, url, uri);
|
||||||
log(`created: ${driveFile._id}`);
|
log(`created: ${driveFile._id}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e;
|
error = e;
|
||||||
|
|
Loading…
Reference in a new issue