Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(files): nsfw scan memory limit #2655

Merged
merged 9 commits into from
Apr 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions components/views/files/controls/Controls.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
multiple
/>
</div>
<TypographyText v-if="status" class="status" :text="status" :size="6" />
<UiProgress v-if="progress < 100" :progress="progress" />
<div class="error-container" v-if="errors.length">
<alert-triangle-icon size="1.3x" />
<div>
Expand Down
5 changes: 5 additions & 0 deletions components/views/files/controls/Controls.less
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
}
}

.status,
.progress {
margin-bottom: @normal-spacing;
}

.error-container {
&:extend(.round-corners);
display: flex;
Expand Down
22 changes: 16 additions & 6 deletions components/views/files/controls/Controls.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export default Vue.extend({
return {
text: '' as string,
errors: [] as Array<string | TranslateResult>,
status: '' as string | TranslateResult,
progress: 100 as number,
}
},
computed: {
Expand Down Expand Up @@ -114,10 +116,6 @@ export default Vue.extend({
sameNameResults.map(async (file: File) => {
// convert heic to jpg for scan. return original heic if sfw
if (await isHeic(file)) {
// prevent crash in case of larger than 2GB heic files. could possibly be broken up into multiple buffers
if (file.size >= this.$Config.arrayBufferLimit) {
return { file, nsfw: false }
}
const buffer = new Uint8Array(await file.arrayBuffer())
const outputBuffer = await convert({
buffer,
Expand Down Expand Up @@ -148,22 +146,25 @@ export default Vue.extend({
files.push(el.file)
}
}
for (const file of files) {
try {
await this.$FileSystem.uploadFile(file)
this.status = this.$t('pages.files.controls.upload', [file.name])
await this.$FileSystem.uploadFile(file, this.setProgress)
} catch (e: any) {
this.errors.push(e?.message ?? '')
}
}
// only update index if files have been updated
if (files.length) {
this.status = this.$t('pages.files.controls.index')
await this.$TextileManager.bucket?.updateIndex(this.$FileSystem.export)
}
this.$store.commit('ui/setIsLoadingFileIndex', false)
this.status = ''
// re-render so new files show up
this.$emit('forceRender')
if (originalFiles.length !== invalidNameResults.length) {
Expand All @@ -179,6 +180,15 @@ export default Vue.extend({
this.errors.push(this.$t('errors.chat.contains_nsfw'))
}
},
/**
* @method setProgress
* @description set progress (% out of 100) while file is being pushed to textile bucket. passed as a callback
* @param num current progress in bytes
* @param size total file size in bytes
*/
setProgress(num: number, size: number) {
this.progress = Math.floor((num / size) * 100)
},
},
})
</script>
Expand Down
2 changes: 1 addition & 1 deletion components/views/files/upload/Upload.vue
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export default Vue.extend({
)
for (const uploadFile of filesToUpload) {
if (uploadFile.file.size <= Config.nsfwByteLimit) {
if (uploadFile.file.size <= Config.nsfwPictureLimit) {
uploadFile.nsfw.checking = true
try {
uploadFile.nsfw.status = await this.$Security.isNSFW(
Expand Down
9 changes: 4 additions & 5 deletions components/views/files/view/View.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<TypographyText :text="$dayjs(file.modified).fromNow()" :size="6" />
</div>
<div class="controls">
<UiLoadersSpinner v-if="load" class="control disabled" spinning />
<UiLoadersSpinner v-if="isLoading" class="control disabled" spinning />
<a
v-else
:data-tooltip="$t('controls.download')"
Expand Down Expand Up @@ -35,14 +35,13 @@
</div>
</div>
</div>
<img v-if="file.thumbnail" class="file-image" :src="file.thumbnail" />
<UiProgress v-if="isLoading" :progress="progress" />
<img v-else-if="file.thumbnail" class="file-image" :src="file.thumbnail" />
<div v-else class="no-preview">
<UiLoadersSpinner v-if="load" class="file-icon" spinning />
<a
v-else
:data-tooltip="$t('controls.download')"
class="has-tooltip has-tooltip-primary has-tooltip-top"
:class="{'disabled' : load}"
:class="{'disabled' : isLoading}"
:href="file.url"
target="_blank"
:download="name"
Expand Down
14 changes: 8 additions & 6 deletions components/views/files/view/View.less
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
inset: 0;
background: rgba(0, 0, 0, 0.75);
z-index: @base-z-index + 35;
padding: @normal-spacing;

.file-image {
display: flex;
Expand All @@ -28,28 +29,29 @@
margin-bottom: 0;
}
}
.progress {
display: flex;
margin: auto;
max-width: 1000px;
}

.file-nav {
width: @full;
width: calc(@full - @xlarge-spacing); //to account for margin
display: flex;
position: absolute;
margin: @normal-spacing;

.file-info {
max-width: 50vw;
.title {
&:extend(.font-secondary);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
&:extend(.ellipsis);
}
}

.controls {
display: flex;
height: fit-content;
margin-left: auto;
margin-right: @large-spacing;
gap: @normal-spacing;

.control {
Expand Down
21 changes: 15 additions & 6 deletions components/views/files/view/View.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,20 @@ export default Vue.extend({
},
data() {
return {
load: false as boolean,
file: undefined as Fil | undefined,
name: '' as string,
progress: 0 as number,
}
},
computed: {
...mapState(['ui']),
isLoading(): boolean {
return this.progress >= 0 && this.progress < 100
},
},
/**
*/
async created() {
this.load = true
this.file = this.$FileSystem.getChild(this.ui.filePreview) as Fil
this.name = this.file?.name
Expand All @@ -47,6 +48,8 @@ export default Vue.extend({
this.file.id,
this.file.name,
this.file.type,
this.file.size,
this.setProgress,
)
}
// file extension according to file name
Expand All @@ -67,24 +70,30 @@ export default Vue.extend({
if (fileExt !== 'svg') {
this.name += '.svg'
}
this.load = false
return
}
// if corrupted txt file
if (!dataExt && fileExt !== 'txt') {
this.name += '.txt'
this.load = false
return
}
// if corrupted file with wrong extension, force the correct one
if (fileExt !== dataExt && dataExt) {
this.name += `.${dataExt}`
}
this.load = false
},
methods: {
/**
* @method setProgress
* @description set progress (% out of 100) while file is being pulled from textile bucket. passed as a callback
* @param num current progress in bytes
* @param size total file size in bytes
*/
setProgress(num: number, size: number) {
this.progress = Math.floor((num / size) * 100)
},
/**
* @method share
* @description Emit to share item - pages/files/browse/index.vue
Expand Down
5 changes: 2 additions & 3 deletions components/views/user/create/Create.vue
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,8 @@ export default Vue.extend({
return
}
// stop upload if picture is too large for nsfw scan.
// Leaving this in place since nsfw profile pictures would be bad
if (files[0].size > this.$Config.nsfwByteLimit) {
// stop upload if picture is too large for nsfw scan
if (files[0].size > this.$Config.nsfwPictureLimit) {
this.error = this.$t('errors.accounts.file_too_large') as string
this.isLoading = false
return
Expand Down
6 changes: 3 additions & 3 deletions config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,9 @@ export const Config = {
routingMiddleware: {
prerequisitesCheckBypass: ['auth', 'setup'],
},
nsfwByteLimit: 1048576 * 8, // 8MB - arbitrary image limit for nsfw scan - binary
personalFilesLimit: 1000000000 * 4, // 4GB - free tier limit - decimal because stoarge systems typically are
arrayBufferLimit: 1073741824 * 2, // 2GB - array buffers larger than this crash - binary
nsfwPictureLimit: 1048576 * 8, // 8MB - images will be scaled down to this value if possible to prevent memory issues - binary
personalFilesLimit: 1000000000 * 4, // 4GB - free tier limit - decimal
nsfwVideoLimit: 1073741824 * 2, // 2GB - videos larger than this crash - binary
regex: {
// identify if a file type is embeddable image
image: '^.*.(apng|avif|gif|jpg|jpeg|jfif|pjpeg|pjp|png|svg|webp)$',
Expand Down
1 change: 1 addition & 0 deletions layouts/files.vue
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
</swiper-slide>
<swiper-slide class="dynamic-content">
<menu-icon
v-if="!showSidebar || $device.isMobile"
class="toggle--sidebar"
size="1.2x"
full-width
Expand Down
1 change: 0 additions & 1 deletion libraries/Files/Directory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,6 @@ export class Directory extends Item {
}

let parent = this.parent

while (!isEqual(parent, null)) {
if (isEqual(parent, child)) {
throw new Error(FileSystemErrors.DIR_PARENT_PARADOX)
Expand Down
9 changes: 3 additions & 6 deletions libraries/Files/TextileFileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ export class TextileFileSystem extends FilSystem {
* @description Upload file to the bucket and create in the file system afterwards
* use uuid as bucket path so files can be renamed freely
* @param {File} file file to be uploaded
* @param {Function} progressCallback used to show progress meter in componment that calls this method
*/
async uploadFile(file: File) {
async uploadFile(file: File, progressCallback: Function) {
const id = uuidv4()
await this.bucket.pushFile(file, id)
await this.bucket.pushFile(file, id, progressCallback)
// read magic byte type, use metadata as backup
const byteType = (await mimeType(file)) as FILE_TYPE
const type = byteType || file.type
Expand Down Expand Up @@ -61,10 +62,6 @@ export class TextileFileSystem extends FilSystem {
type: FILE_TYPE,
): Promise<string | undefined> {
if (await isHeic(file)) {
// prevent crash in case of larger than 2GB heic files. could possibly be broken up into multiple buffers
if (file.size >= Config.arrayBufferLimit) {
return
}
const buffer = new Uint8Array(await file.arrayBuffer())
const outputBuffer = await convert({
buffer,
Expand Down
56 changes: 50 additions & 6 deletions libraries/Files/remote/textile/Bucket.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Buckets, PushPathResult, RemovePathResponse, Root } from '@textile/hub'
import { Buckets, RemovePathResponse, Root } from '@textile/hub'
import { RFM } from '../abstracts/RFM.abstract'
import { RFMInterface } from '../interface/RFM.interface'
import { Config } from '~/config'
Expand Down Expand Up @@ -107,34 +107,78 @@ export class Bucket extends RFM implements RFMInterface {
/**
* @method pushFile
* @description Add file to bucket
* stream upload syntax - https://textileio.github.io/js-textile/docs/hub.buckets.pushpath#example-2
* @param {File} file file to be uploaded
* @returns Promise whether it was uploaded or not
* @param {string} path uuid to maintain unique bucket paths
* @param {Function} progressCallback used to show progress meter in componment that calls this method
*/
async pushFile(file: File, id: string): Promise<PushPathResult> {
async pushFile(file: File, path: string, progressCallback: Function) {
if (!this.buckets || !this.key) {
throw new Error('Bucket or bucket key not found')
}
return await this.buckets.pushPath(this.key, id, file)

await this.buckets.pushPath(
this.key,
path,
{
path,
content: this._getStream(file),
},
{
progress: (num) => {
progressCallback(num, file.size)
},
},
)
}

private _getStream(file: File) {
const reader = file.stream().getReader()
const stream = new ReadableStream({
start(controller) {
function push() {
return reader
.read()
.then(({ done, value }: { done: boolean; value: Uint8Array }) => {
if (done) {
controller.close()
return
}
controller.enqueue(value)
push()
})
}
push()
},
})
return stream
}

/**
* @method pullFile
* @description fetch encrypted file from bucket
* @param {string} id file path in bucket
* @param {string} type file mime type
* @param {Function} progressCallback used to show progress meter in componment that calls this method
* @returns Promise of File
*/
async pullFile(
id: string,
name: string,
type: string,
): Promise<File | undefined> {
size: number,
progressCallback: Function,
): Promise<File> {
if (!this.buckets || !this.key) {
throw new Error('Bucket or bucket key not found')
}

const data = []
for await (const bytes of this.buckets.pullPath(this.key, id)) {
for await (const bytes of this.buckets.pullPath(this.key, id, {
progress: (num) => {
progressCallback(num, size)
},
})) {
data.push(bytes)
}
// if type is unknown(generic), then don't use in File constructor
Expand Down
2 changes: 2 additions & 0 deletions locales/en-US.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ export default {
controls: {
new_file: 'New File',
name_folder: 'Name Folder...',
upload: 'Uploading {0}',
index: 'Updating remote index',
},
browse: {
files: 'Files',
Expand Down
Loading