mirror of https://github.com/VERT-sh/VERT.git
feat: warn of large file sizes due to browser limits
browsers / devices will have different arraybuffer sizes. if exceeded, block conversion or disable certain ones (video to audio)
This commit is contained in:
parent
8a958aed46
commit
0e5f549804
|
|
@ -52,6 +52,7 @@
|
|||
"extracted": "Extracted {extract_count} files from {filename}. {ignore_count} items were ignored.",
|
||||
"extract_error": "Error extracting {filename}: {error}"
|
||||
},
|
||||
"large_file_warning": "Due to browser / device limitations, video to audio conversion is disabled for this file as it is larger than {limit}GB. We recommend using Firefox or Safari for files of this size since they have less limitations.",
|
||||
"external_warning": {
|
||||
"title": "External server warning",
|
||||
"text": "If you choose to convert into a video format, those files will be uploaded to an external server to be converted. Do you want to continue?",
|
||||
|
|
@ -255,7 +256,8 @@
|
|||
"ffmpeg": "Error loading FFmpeg, some features may not work as expected.",
|
||||
"pandoc": "Error loading Pandoc worker, document conversion may not work as expected.",
|
||||
"no_audio": "No audio stream found.",
|
||||
"invalid_rate": "Invalid sample rate specified: {rate}Hz"
|
||||
"invalid_rate": "Invalid sample rate specified: {rate}Hz",
|
||||
"file_too_large": "This file exceeds the {limit}GB browser / device limit. Try Firefox or Safari to convert this large file, which typically have higher limits."
|
||||
}
|
||||
},
|
||||
"privacy": {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import { ChevronDown, SearchIcon } from "lucide-svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { quintOut } from "svelte/easing";
|
||||
import type { VertFile } from "$lib/types";
|
||||
|
||||
type Props = {
|
||||
categories: Categories;
|
||||
|
|
@ -15,6 +16,7 @@
|
|||
onselect?: (option: string) => void;
|
||||
disabled?: boolean;
|
||||
dropdownSize?: "default" | "large" | "small";
|
||||
file?: VertFile;
|
||||
};
|
||||
|
||||
let {
|
||||
|
|
@ -24,6 +26,7 @@
|
|||
onselect,
|
||||
disabled,
|
||||
dropdownSize = "default",
|
||||
file,
|
||||
}: Props = $props();
|
||||
let open = $state(false);
|
||||
let dropdown = $state<HTMLDivElement>();
|
||||
|
|
@ -67,6 +70,15 @@
|
|||
);
|
||||
if (from === ".gif") finalCategories.push("video");
|
||||
|
||||
// filter out categories that can't handle large files (due to browser/device limitations)
|
||||
if (file && file.isLarge()) {
|
||||
// if file is large video, disable audio conversion
|
||||
if (rootCategory === "video")
|
||||
finalCategories = finalCategories.filter(
|
||||
(cat) => cat !== "audio",
|
||||
);
|
||||
}
|
||||
|
||||
return finalCategories;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -15,3 +15,5 @@ export const CONTACT_EMAIL = "hello@vert.sh";
|
|||
// i'm not entirely sure this should be in consts.ts, but it is technically a constant as .env is static for VERT
|
||||
export const DISABLE_ALL_EXTERNAL_REQUESTS =
|
||||
PUB_DISABLE_ALL_EXTERNAL_REQUESTS === "true";
|
||||
|
||||
export const GB = 1024 * 1024 * 1024;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { m } from "$lib/paraglide/messages";
|
|||
import sanitizeHtml from "sanitize-html";
|
||||
import { unzip } from "fflate";
|
||||
import { ToastManager } from "$lib/toast/index.svelte";
|
||||
import { GB } from "$lib/consts";
|
||||
|
||||
class Files {
|
||||
public files = $state<VertFile[]>([]);
|
||||
|
|
@ -259,7 +260,20 @@ class Files {
|
|||
this.files.push(vf);
|
||||
this._addThumbnail(vf);
|
||||
|
||||
const isVideo = converter.name === "vertd";
|
||||
const convName = converter.name;
|
||||
if (file.size > MAX_ARRAY_BUFFER_SIZE && convName === "vertd") {
|
||||
ToastManager.add({
|
||||
type: "warning",
|
||||
message: m["convert.large_file_warning"]({
|
||||
limit: (MAX_ARRAY_BUFFER_SIZE / GB).toFixed(2),
|
||||
}),
|
||||
durations: {
|
||||
stay: 10000,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const isVideo = convName === "vertd";
|
||||
const acceptedExternalWarning =
|
||||
localStorage.getItem("acceptedExternalWarning") === "true";
|
||||
if (isVideo && !acceptedExternalWarning && !this._warningShown) {
|
||||
|
|
@ -481,3 +495,66 @@ export function sanitize(
|
|||
allowedSchemes: ["http", "https", "mailto", "blob"],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Binary search for a max value without knowing the exact value, only that it
|
||||
* can be under or over It dose not test every number but instead looks for
|
||||
* 1,2,4,8,16,32,64,128,96,95 to figure out that you thought about #96 from
|
||||
* 0-infinity
|
||||
*
|
||||
* @example findFirstPositive(x => matchMedia(`(max-resolution: ${x}dpi)`).matches)
|
||||
* @author Jimmy Wärting
|
||||
* @see {@link https://stackoverflow.com/a/72124984/1008999}
|
||||
* @param {function} f The function to run the test on (should return truthy or falsy values)
|
||||
* @param {bigint} [b=1] Where to start looking from
|
||||
* @param {function} d privately used to calculate the next value to test
|
||||
* @returns {bigint} Integer
|
||||
*/
|
||||
function findFirstPositive(
|
||||
f: (x: bigint) => number,
|
||||
b = 1n,
|
||||
d = (e: bigint, g: bigint, c?: bigint): bigint =>
|
||||
g < e
|
||||
? -1n
|
||||
: 0 < f((c = (e + g) >> 1n))
|
||||
? c == e || 0 >= f(c - 1n)
|
||||
? c
|
||||
: d(e, c - 1n)
|
||||
: d(c + 1n, g),
|
||||
): bigint {
|
||||
for (; 0 >= f(b); b <<= 1n);
|
||||
return d(b >> 1n, b) - 1n;
|
||||
}
|
||||
|
||||
export const getMaxArrayBufferSize = (): number => {
|
||||
if (typeof window === "undefined") return 2 * GB; // default for SSR
|
||||
|
||||
// check cache first
|
||||
const cached = localStorage.getItem("maxArrayBufferSize");
|
||||
if (cached) {
|
||||
const parsed = Number(cached);
|
||||
log(
|
||||
["converters"],
|
||||
`using cached max ArrayBuffer size: ${parsed} bytes`,
|
||||
);
|
||||
if (!isNaN(parsed) && parsed > 0) return parsed;
|
||||
}
|
||||
|
||||
// detect max size using binary search
|
||||
const maxSize = findFirstPositive((x) => {
|
||||
try {
|
||||
new ArrayBuffer(Number(x));
|
||||
return 0; // false = can allocate
|
||||
} catch {
|
||||
return 1; // true = cannot allocate
|
||||
}
|
||||
});
|
||||
|
||||
const result = Number(maxSize);
|
||||
localStorage.setItem("maxArrayBufferSize", result.toString());
|
||||
log(["converters"], `detected max ArrayBuffer size: ${result} bytes`);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const MAX_ARRAY_BUFFER_SIZE = getMaxArrayBufferSize();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import type { Component } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
export type ToastType = "success" | "error" | "info" | "warning";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { byNative, converters } from "$lib/converters";
|
||||
import type { Converter } from "$lib/converters/converter.svelte";
|
||||
import { error } from "$lib/logger";
|
||||
import { m } from "$lib/paraglide/messages";
|
||||
import { ToastManager } from "$lib/toast/index.svelte";
|
||||
import type { Component } from "svelte";
|
||||
import { MAX_ARRAY_BUFFER_SIZE } from "$lib/store/index.svelte";
|
||||
|
||||
export class VertFile {
|
||||
public id: string = Math.random().toString(36).slice(2, 8);
|
||||
|
|
@ -63,6 +63,17 @@ export class VertFile {
|
|||
return converter;
|
||||
}
|
||||
|
||||
public isLarge(): boolean {
|
||||
return this.file.size > MAX_ARRAY_BUFFER_SIZE;
|
||||
}
|
||||
|
||||
public supportsStreaming(): boolean {
|
||||
// only vertd (video/gif -> video/gif) supports streaming
|
||||
// rest of converters need entire file in memory, limited by ArrayBuffer limits
|
||||
const converter = this.findConverter();
|
||||
return converter?.name === "vertd";
|
||||
}
|
||||
|
||||
constructor(file: File, to: string, blobUrl?: string) {
|
||||
const ext = file.name.split(".").pop();
|
||||
const newFile = new File(
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
import Panel from "$lib/components/visual/Panel.svelte";
|
||||
import ProgressBar from "$lib/components/visual/ProgressBar.svelte";
|
||||
import Tooltip from "$lib/components/visual/Tooltip.svelte";
|
||||
import { categories, converters, byNative } from "$lib/converters";
|
||||
import { categories, converters } from "$lib/converters";
|
||||
import {
|
||||
effects,
|
||||
files,
|
||||
|
|
@ -29,6 +29,8 @@
|
|||
} from "lucide-svelte";
|
||||
import { m } from "$lib/paraglide/messages";
|
||||
import { Settings } from "$lib/sections/settings/index.svelte";
|
||||
import { MAX_ARRAY_BUFFER_SIZE } from "$lib/store/index.svelte";
|
||||
import { GB } from "$lib/consts";
|
||||
|
||||
let processedFileIds = $state(new Set<string>());
|
||||
|
||||
|
|
@ -220,6 +222,7 @@
|
|||
{@const formatInfo = currentConverter.supportedFormats.find(
|
||||
(f) => f.name === file.from,
|
||||
)}
|
||||
{@const isLarge = file.isLarge()}
|
||||
{#if formatInfo && !formatInfo.fromSupported}
|
||||
<div
|
||||
class="h-full flex flex-col text-center justify-center text-failure"
|
||||
|
|
@ -231,6 +234,19 @@
|
|||
{m["convert.errors.format_output_only"]()}
|
||||
</p>
|
||||
</div>
|
||||
{:else if isLarge && !file.supportsStreaming()}
|
||||
<div
|
||||
class="h-full flex flex-col text-center justify-center text-failure"
|
||||
>
|
||||
<p class="font-body font-bold">
|
||||
{m["convert.errors.cant_convert"]()}
|
||||
</p>
|
||||
<p class="font-normal">
|
||||
{m["workers.errors.file_too_large"]({
|
||||
limit: (MAX_ARRAY_BUFFER_SIZE / GB).toFixed(2),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
{:else if currentConverter.status === "downloading"}
|
||||
<div
|
||||
class="h-full flex flex-col text-center justify-center text-failure"
|
||||
|
|
@ -347,6 +363,7 @@
|
|||
bind:selected={file.to}
|
||||
onselect={(option) =>
|
||||
handleSelect(option, file)}
|
||||
{file}
|
||||
/>
|
||||
<div
|
||||
class="w-full flex items-center justify-between"
|
||||
|
|
|
|||
Loading…
Reference in New Issue