diff --git a/cli/clean-cached-remote-files.js b/cli/clean-cached-remote-files.js index e4db37ef9..a9c38a4cd 100644 --- a/cli/clean-cached-remote-files.js +++ b/cli/clean-cached-remote-files.js @@ -8,7 +8,8 @@ const { default: User } = require('../built/models/user'); const q = { 'metadata._user.host': { $ne: null - } + }, + 'metadata.isMetaOnly': false }; async function main() { @@ -56,8 +57,7 @@ async function main() { DriveFile.update({ _id: file._id }, { $set: { - 'metadata.deletedAt': new Date(), - 'metadata.isExpired': true + 'metadata.isMetaOnly': true } }) ]).then(async () => { diff --git a/locales/ja.yml b/locales/ja.yml index 7d818193b..49046d26e 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -855,6 +855,8 @@ mobile/views/pages/settings.vue: behavior: "動作" fetch-on-scroll: "スクロールで自動読み込み" disable-via-mobile: "「モバイルからの投稿」フラグを付けない" + load-raw-images: "添付された画像を高画質で表示する" + load-remote-media: "リモートサーバーのメディアを表示する" twitter: "Twitter連携" twitter-connect: "Twitterアカウントに接続する" twitter-reconnect: "再接続する" diff --git a/src/client/app/mobile/views/components/media-image.vue b/src/client/app/mobile/views/components/media-image.vue index c4622b01a..c2f9c66e8 100644 --- a/src/client/app/mobile/views/components/media-image.vue +++ b/src/client/app/mobile/views/components/media-image.vue @@ -16,13 +16,18 @@ export default Vue.extend({ } }, computed: { - lightmode(): boolean { - return this.$store.state.device.lightmode; - }, 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 { '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 }; } } diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue index 270c9f405..3bb25f88f 100644 --- a/src/client/app/mobile/views/pages/settings.vue +++ b/src/client/app/mobile/views/pages/settings.vue @@ -59,6 +59,14 @@ %i18n:@disable-via-mobile% +
+ %i18n:@load-raw-images% +
+ +
+ %i18n:@load-remote-media% +
+
%i18n:@i-am-under-limited-internet%
@@ -166,6 +174,11 @@ export default Vue.extend({ 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: { get() { return this.$store.state.device.lang; }, 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) { this.$store.dispatch('settings/set', { key: 'circleIcons', diff --git a/src/client/app/store.ts b/src/client/app/store.ts index 74a5852c1..e300d31d8 100644 --- a/src/client/app/store.ts +++ b/src/client/app/store.ts @@ -13,7 +13,9 @@ const defaultSettings = { gradientWindowHeader: false, showReplyTarget: true, showMyRenotes: true, - showRenotedMyNotes: true + showRenotedMyNotes: true, + loadRemoteMedia: true, + disableViaMobile: false }; const defaultDeviceSettings = { @@ -26,6 +28,7 @@ const defaultDeviceSettings = { preventUpdate: false, debug: false, lightmode: false, + loadRawImages: false, postStyle: 'standard' }; diff --git a/src/config/types.ts b/src/config/types.ts index dff3f7d37..910c03c2c 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -41,6 +41,8 @@ export type Source = { secret_key: string; }; + preventCacheRemoteFiles: boolean; + /** * ゴーストアカウントのID */ diff --git a/src/models/drive-file.ts b/src/models/drive-file.ts index 8a18567dc..a3a567038 100644 --- a/src/models/drive-file.ts +++ b/src/models/drive-file.ts @@ -32,7 +32,7 @@ export type IMetadata = { uri?: string; url?: string; deletedAt?: Date; - isExpired?: boolean; + isMetaOnly?: boolean; }; export type IDriveFile = { @@ -155,7 +155,8 @@ export const pack = ( _target = Object.assign(_target, _file.metadata); _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 = {}; diff --git a/src/server/file/send-drive-file.ts b/src/server/file/send-drive-file.ts index d613a3aa5..e04400317 100644 --- a/src/server/file/send-drive-file.ts +++ b/src/server/file/send-drive-file.ts @@ -33,11 +33,12 @@ export default async function(ctx: Koa.Context) { if (file.metadata.deletedAt) { ctx.status = 410; - if (file.metadata.isExpired) { - await send(ctx, '/cache-expired.png', { root: assets }); - } else { - await send(ctx, '/tombstone.png', { root: assets }); - } + await send(ctx, '/tombstone.png', { root: assets }); + return; + } + + if (file.metadata.isMetaOnly) { + ctx.status = 204; return; } diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts index 8b1d3eef0..413c91417 100644 --- a/src/services/drive/add-file.ts +++ b/src/services/drive/add-file.ts @@ -104,6 +104,7 @@ export default async function( comment: string = null, folderId: mongodb.ObjectID = null, force: boolean = false, + metaOnly: boolean = false, url: string = null, uri: string = null ): Promise { @@ -170,38 +171,40 @@ export default async function( } //#region Check drive usage - const usage = await DriveFile - .aggregate([{ - $match: { - 'metadata.userId': user._id, - 'metadata.deletedAt': { $exists: false } - } - }, { - $project: { - length: true - } - }, { - $group: { - _id: null, - usage: { $sum: '$length' } - } - }]) - .then((aggregates: any[]) => { - if (aggregates.length > 0) { - return aggregates[0].usage; - } - return 0; - }); + if (!metaOnly) { + const usage = await DriveFile + .aggregate([{ + $match: { + 'metadata.userId': user._id, + 'metadata.deletedAt': { $exists: false } + } + }, { + $project: { + length: true + } + }, { + $group: { + _id: null, + usage: { $sum: '$length' } + } + }]) + .then((aggregates: any[]) => { + if (aggregates.length > 0) { + return aggregates[0].usage; + } + return 0; + }); - log(`drive usage is ${usage}`); + log(`drive usage is ${usage}`); - // If usage limit exceeded - if (usage + size > user.driveCapacity) { - if (isLocalUser(user)) { - throw 'no-free-space'; - } else { - // (アバターまたはバナーを含まず)最も古いファイルを削除する - deleteOldFile(user); + // If usage limit exceeded + if (usage + size > user.driveCapacity) { + if (isLocalUser(user)) { + throw 'no-free-space'; + } else { + // (アバターまたはバナーを含まず)最も古いファイルを削除する + deleteOldFile(user); + } } } //#endregion @@ -270,8 +273,6 @@ export default async function( const [folder] = await Promise.all([fetchFolder(), propPromises]); - const readable = fs.createReadStream(path); - const metadata = { userId: user._id, _user: { @@ -279,7 +280,8 @@ export default async function( }, folderId: folder !== null ? folder._id : null, comment: comment, - properties: properties + properties: properties, + isMetaOnly: metaOnly } as IMetadata; if (url !== null) { @@ -290,7 +292,16 @@ export default async function( metadata.uri = uri; } - const driveFile = await (writeChunks(detectedName, readable, mime, metadata) as Promise); + 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); log(`drive file has been created ${driveFile._id}`); @@ -300,13 +311,15 @@ export default async function( publishDriveStream(user._id, 'file_created', packedFile); }); - try { - const thumb = await genThumbnail(driveFile); - if (thumb) { - await writeThumbnailChunks(detectedName, thumb, driveFile._id); + if (!metaOnly) { + try { + const thumb = await genThumbnail(driveFile); + if (thumb) { + await writeThumbnailChunks(detectedName, thumb, driveFile._id); + } + } catch (e) { + // noop } - } catch (e) { - // noop } return driveFile; diff --git a/src/services/drive/upload-from-url.ts b/src/services/drive/upload-from-url.ts index ad2620c03..e216ca603 100644 --- a/src/services/drive/upload-from-url.ts +++ b/src/services/drive/upload-from-url.ts @@ -1,14 +1,17 @@ +import * as fs from 'fs'; import * as URL from 'url'; -import { IDriveFile, validateFileName } from '../../models/drive-file'; -import create from './add-file'; + import * as debug from 'debug'; import * as tmp from 'tmp'; -import * as fs from 'fs'; 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'); -export default async (url, user, folderId = null, uri = null): Promise => { +export default async (url: string, user, folderId = null, uri: string = null): Promise => { log(`REQUESTED: ${url}`); let name = URL.parse(url).pathname.split('/').pop(); @@ -43,7 +46,7 @@ export default async (url, user, folderId = null, uri = null): Promise