fix(files): prevent autosave data loss on load lag and binary files
Guard FilesView autosave until the selected file has finished loading, refuse binary/PDF/office/archive text saves, and add a persisted global autoSaveEnabled setting (default true) under Settings → General. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
This commit is contained in:
co-authored by
Serhii Dziupin
parent
a4c7bac303
commit
5b727ed53c
@@ -28,7 +28,7 @@ import { copyTextToClipboard } from '@/lib/clipboard';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { ensurePierreThemeRegistered } from '@/lib/shiki/appThemeRegistry';
|
||||
import { getDefaultTheme } from '@/lib/theme/themes';
|
||||
import { getImageMimeType, getLanguageFromExtension, isImageFile } from '@/lib/toolHelpers';
|
||||
import { getImageMimeType, getLanguageFromExtension, isBinaryFile, isImageFile, isPdfFile, isSvgFile, looksLikeBinaryText } from '@/lib/toolHelpers';
|
||||
import type { FileListEntry, FileSearchResult } from '@/lib/api/types';
|
||||
import { runtimeFetch } from '@/lib/runtime-fetch';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -98,6 +98,7 @@ export const MobileFilesSurface: React.FC<MobileFilesSurfaceProps> = ({ onClose
|
||||
const [imageSrc, setImageSrc] = React.useState('');
|
||||
const [fileError, setFileError] = React.useState<string | null>(null);
|
||||
const [isLoadingFile, setIsLoadingFile] = React.useState(false);
|
||||
const [binaryBlocked, setBinaryBlocked] = React.useState(false);
|
||||
const directoryLoadRequestIdRef = React.useRef(0);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -174,8 +175,9 @@ export const MobileFilesSurface: React.FC<MobileFilesSurfaceProps> = ({ onClose
|
||||
setFileContent('');
|
||||
setImageSrc('');
|
||||
setFileError(null);
|
||||
setBinaryBlocked(false);
|
||||
|
||||
if (isImageFile(route.path) && !route.path.toLowerCase().endsWith('.svg')) {
|
||||
if (isImageFile(route.path) && !isSvgFile(route.path)) {
|
||||
let cancelled = false;
|
||||
let objectUrl = '';
|
||||
setIsLoadingFile(true);
|
||||
@@ -203,6 +205,14 @@ export const MobileFilesSurface: React.FC<MobileFilesSurfaceProps> = ({ onClose
|
||||
};
|
||||
}
|
||||
|
||||
// Never load PDF/office/archives/etc. as UTF-8 text — that path can corrupt originals
|
||||
// if a future write path is added, and it shows gibberish in the viewer.
|
||||
if (isBinaryFile(route.path) || isPdfFile(route.path)) {
|
||||
setBinaryBlocked(true);
|
||||
setIsLoadingFile(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!files.readFile) {
|
||||
setFileError(t('mobile.files.error.readUnavailable'));
|
||||
setIsLoadingFile(false);
|
||||
@@ -214,6 +224,11 @@ export const MobileFilesSurface: React.FC<MobileFilesSurfaceProps> = ({ onClose
|
||||
void files.readFile(route.path)
|
||||
.then((result) => {
|
||||
if (cancelled) return;
|
||||
if (looksLikeBinaryText(result.content)) {
|
||||
setBinaryBlocked(true);
|
||||
setFileContent('');
|
||||
return;
|
||||
}
|
||||
setFileContent(result.content.length > MAX_MOBILE_FILE_CHARS
|
||||
? `${result.content.slice(0, MAX_MOBILE_FILE_CHARS)}\n\n${t('mobile.files.file.truncated')}`
|
||||
: result.content);
|
||||
@@ -263,6 +278,7 @@ export const MobileFilesSurface: React.FC<MobileFilesSurfaceProps> = ({ onClose
|
||||
imageSrc={imageSrc}
|
||||
error={fileError}
|
||||
isLoading={isLoadingFile}
|
||||
binaryBlocked={binaryBlocked}
|
||||
onBack={() => setRoute({ type: 'browser', directory: route.returnDirectory })}
|
||||
onCopyPath={() => void handleCopyPath(route.path)}
|
||||
onCopyContent={() => void handleCopyContent()}
|
||||
@@ -413,10 +429,11 @@ const MobileFileDetail: React.FC<{
|
||||
imageSrc: string;
|
||||
error: string | null;
|
||||
isLoading: boolean;
|
||||
binaryBlocked: boolean;
|
||||
onBack: () => void;
|
||||
onCopyPath: () => void;
|
||||
onCopyContent: () => void;
|
||||
}> = ({ path, content, imageSrc, error, isLoading, onBack, onCopyPath, onCopyContent }) => {
|
||||
}> = ({ path, content, imageSrc, error, isLoading, binaryBlocked, onBack, onCopyPath, onCopyContent }) => {
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
@@ -433,7 +450,7 @@ const MobileFileDetail: React.FC<{
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 className="truncate typography-ui-header text-foreground">{getNameFromPath(path)}</h2>
|
||||
</div>
|
||||
{!isImageFile(path) ? (
|
||||
{!isImageFile(path) && !binaryBlocked ? (
|
||||
<Button type="button" variant="ghost" size="icon" onClick={onCopyContent} aria-label={t('mobile.files.copyContentAria')}>
|
||||
<RiFileCopyLine className="size-4" />
|
||||
</Button>
|
||||
@@ -455,6 +472,11 @@ const MobileFileDetail: React.FC<{
|
||||
<ScrollShadow className="h-full overflow-auto p-4">
|
||||
<img src={`data:${getImageMimeType(path)};utf8,${encodeURIComponent(content)}`} alt={getNameFromPath(path)} className="mx-auto max-h-full max-w-full rounded-lg object-contain" />
|
||||
</ScrollShadow>
|
||||
) : binaryBlocked ? (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-2 p-6 text-center">
|
||||
<div className="typography-ui-header text-foreground">{t('filesView.editor.cannotPreviewBinary')}</div>
|
||||
<div className="max-w-sm typography-ui text-muted-foreground">{t('filesView.editor.binaryFileDescription')}</div>
|
||||
</div>
|
||||
) : (
|
||||
<MobileTextFile path={path} content={content} />
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user