1
0
mirror of https://github.com/mifi/lossless-cut.git synced 2024-11-22 02:12:30 +01:00

check if output file is writable before exporting

if it exists
so we can inform user
This commit is contained in:
Mikael Finstad 2024-09-18 19:53:47 +08:00
parent e7f2d39d36
commit 6c9b1ba708
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26
4 changed files with 63 additions and 28 deletions

View File

@ -17,7 +17,7 @@ import { SweetAlertOptions } from 'sweetalert2';
import theme from './theme'; import theme from './theme';
import useTimelineScroll from './hooks/useTimelineScroll'; import useTimelineScroll from './hooks/useTimelineScroll';
import useUserSettingsRoot from './hooks/useUserSettingsRoot'; import useUserSettingsRoot from './hooks/useUserSettingsRoot';
import useFfmpegOperations from './hooks/useFfmpegOperations'; import useFfmpegOperations, { OutputNotWritableError } from './hooks/useFfmpegOperations';
import useKeyframes from './hooks/useKeyframes'; import useKeyframes from './hooks/useKeyframes';
import useWaveform from './hooks/useWaveform'; import useWaveform from './hooks/useWaveform';
import useKeyboard from './hooks/useKeyboard'; import useKeyboard from './hooks/useKeyboard';
@ -71,12 +71,13 @@ import {
calcShouldShowWaveform, calcShouldShowKeyframes, mediaSourceQualities, isExecaError, getStdioString, calcShouldShowWaveform, calcShouldShowKeyframes, mediaSourceQualities, isExecaError, getStdioString,
isMuxNotSupported, isMuxNotSupported,
getDownloadMediaOutPath, getDownloadMediaOutPath,
isAbortedError,
} from './util'; } from './util';
import { toast, errorToast, showPlaybackFailedMessage } from './swal'; import { toast, errorToast, showPlaybackFailedMessage } from './swal';
import { adjustRate } from './util/rate-calculator'; import { adjustRate } from './util/rate-calculator';
import { askExtractFramesAsImages } from './dialogs/extractFrames'; import { askExtractFramesAsImages } from './dialogs/extractFrames';
import { askForHtml5ifySpeed } from './dialogs/html5ify'; import { askForHtml5ifySpeed } from './dialogs/html5ify';
import { askForOutDir, askForImportChapters, promptTimecode, askForFileOpenAction, confirmExtractAllStreamsDialog, showCleanupFilesDialog, showDiskFull, showExportFailedDialog, showConcatFailedDialog, openYouTubeChaptersDialog, showRefuseToOverwrite, openDirToast, openExportFinishedToast, openConcatFinishedToast, showOpenDialog, showMuxNotSupported, promptDownloadMediaUrl, CleanupChoicesType } from './dialogs'; import { askForOutDir, askForImportChapters, promptTimecode, askForFileOpenAction, confirmExtractAllStreamsDialog, showCleanupFilesDialog, showDiskFull, showExportFailedDialog, showConcatFailedDialog, openYouTubeChaptersDialog, showRefuseToOverwrite, openDirToast, openExportFinishedToast, openConcatFinishedToast, showOpenDialog, showMuxNotSupported, promptDownloadMediaUrl, CleanupChoicesType, showOutputNotWritable } from './dialogs';
import { openSendReportDialog } from './reporting'; import { openSendReportDialog } from './reporting';
import { fallbackLng } from './i18n'; import { fallbackLng } from './i18n';
import { findSegmentsAtCursor, sortSegments, convertSegmentsToChapters, hasAnySegmentOverlap, isDurationValid, playOnlyCurrentSegment, getSegmentTags } from './segments'; import { findSegmentsAtCursor, sortSegments, convertSegmentsToChapters, hasAnySegmentOverlap, isDurationValid, playOnlyCurrentSegment, getSegmentTags } from './segments';
@ -863,16 +864,11 @@ function App() {
openConcatFinishedToast({ filePath: outPath, notices, warnings }); openConcatFinishedToast({ filePath: outPath, notices, warnings });
} }
} catch (err) { } catch (err) {
if (err instanceof DirectoryAccessDeclinedError || isAbortedError(err)) return;
showOsNotification(i18n.t('Failed to merge')); showOsNotification(i18n.t('Failed to merge'));
if (err instanceof DirectoryAccessDeclinedError) return;
if (isExecaError(err)) { if (isExecaError(err)) {
if (err.killed) {
// assume execa killed (aborted by user)
return;
}
console.log('stdout:', getStdioString(err.stdout)); console.log('stdout:', getStdioString(err.stdout));
console.error('stderr:', getStdioString(err.stderr)); console.error('stderr:', getStdioString(err.stderr));
@ -884,12 +880,15 @@ function App() {
showMuxNotSupported(); showMuxNotSupported();
return; return;
} }
const reportState = { includeAllStreams, streams, outFormat, outFileName, segmentsToChapters }; }
handleConcatFailed(err, reportState);
if (err instanceof OutputNotWritableError) {
showOutputNotWritable();
return; return;
} }
handleError(err); const reportState = { includeAllStreams, streams, outFormat, outFileName, segmentsToChapters };
handleConcatFailed(err, reportState);
} finally { } finally {
setWorking(undefined); setWorking(undefined);
setCutProgress(undefined); setCutProgress(undefined);
@ -1105,17 +1104,14 @@ function App() {
if (cleanupChoices.cleanupAfterExport) await cleanupFilesWithDialog(); if (cleanupChoices.cleanupAfterExport) await cleanupFilesWithDialog();
} catch (err) { } catch (err) {
if (isExecaError(err)) { if (isAbortedError(err)) return;
if (err.killed) {
// assume execa killed (aborted by user)
return;
}
console.log('stdout:', getStdioString(err.stdout));
console.error('stderr:', getStdioString(err.stderr));
showOsNotification(i18n.t('Failed to export')); showOsNotification(i18n.t('Failed to export'));
if (isExecaError(err)) {
console.log('stdout:', getStdioString(err.stdout));
console.error('stderr:', getStdioString(err.stderr));
if (isOutOfSpaceError(err)) { if (isOutOfSpaceError(err)) {
showDiskFull(); showDiskFull();
return; return;
@ -1124,12 +1120,14 @@ function App() {
showMuxNotSupported(); showMuxNotSupported();
return; return;
} }
handleExportFailed(err); }
if (err instanceof OutputNotWritableError) {
showOutputNotWritable();
return; return;
} }
showOsNotification(i18n.t('Failed to export')); handleExportFailed(err);
handleError(err);
} finally { } finally {
setWorking(undefined); setWorking(undefined);
setCutProgress(undefined); setCutProgress(undefined);
@ -1258,7 +1256,7 @@ function App() {
// then try to open project from source file dir // then try to open project from source file dir
const sameDirEdlFilePath = getEdlFilePath(fp); const sameDirEdlFilePath = getEdlFilePath(fp);
// MAS only allows fs.stat (fs-extra.exists) if we don't have access to input dir yet, so check first if the file exists, // MAS only allows fs.access (fs-extra.exists) if we don't have access to input dir yet, so check first if the file exists,
// so we don't need to annoy the user by asking for permission if the project file doesn't exist // so we don't need to annoy the user by asking for permission if the project file doesn't exist
if (await exists(sameDirEdlFilePath)) { if (await exists(sameDirEdlFilePath)) {
// Ok, the file exists. now we have to ask the user, because we need to read that file // Ok, the file exists. now we have to ask the user, because we need to read that file

View File

@ -168,6 +168,13 @@ export async function showMuxNotSupported() {
}); });
} }
export async function showOutputNotWritable() {
await Swal.fire({
icon: 'error',
text: i18n.t('You are not allowed to write the output file. This probably means that the file already exists with the wrong permissions, or you don\'t have write permissions to the output folder.'),
});
}
export async function showRefuseToOverwrite() { export async function showRefuseToOverwrite() {
await Swal.fire({ await Swal.fire({
icon: 'warning', icon: 'warning',

View File

@ -14,8 +14,15 @@ import { AvoidNegativeTs, Html5ifyMode } from '../../../../types';
import { AllFilesMeta, Chapter, CopyfileStreams, CustomTagsByFile, ParamsByStreamId, SegmentToExport } from '../types'; import { AllFilesMeta, Chapter, CopyfileStreams, CustomTagsByFile, ParamsByStreamId, SegmentToExport } from '../types';
const { join, resolve, dirname } = window.require('path'); const { join, resolve, dirname } = window.require('path');
const { pathExists } = window.require('fs-extra'); const { writeFile, mkdir, access, constants: { F_OK, W_OK } } = window.require('fs/promises');
const { writeFile, mkdir } = window.require('fs/promises');
export class OutputNotWritableError extends Error {
constructor() {
super();
this.name = 'OutputNotWritableError';
}
}
async function writeChaptersFfmetadata(outDir: string, chapters: Chapter[] | undefined) { async function writeChaptersFfmetadata(outDir: string, chapters: Chapter[] | undefined) {
if (!chapters || chapters.length === 0) return undefined; if (!chapters || chapters.length === 0) return undefined;
@ -60,6 +67,15 @@ async function tryDeleteFiles(paths: string[]) {
return pMap(paths, (path) => unlinkWithRetry(path).catch((err) => console.error('Failed to delete', path, err)), { concurrency: 5 }); return pMap(paths, (path) => unlinkWithRetry(path).catch((err) => console.error('Failed to delete', path, err)), { concurrency: 5 });
} }
async function pathExists(path: string) {
try {
await access(path, F_OK);
return true;
} catch {
return false;
}
}
function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, needSmartCut, enableOverwriteOutput, outputPlaybackRate, cutFromAdjustmentFrames, appendLastCommandsLog, smartCutCustomBitrate }: { function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, needSmartCut, enableOverwriteOutput, outputPlaybackRate, cutFromAdjustmentFrames, appendLastCommandsLog, smartCutCustomBitrate }: {
filePath: string | undefined, filePath: string | undefined,
treatInputFileModifiedTimeAsStart: boolean | null | undefined, treatInputFileModifiedTimeAsStart: boolean | null | undefined,
@ -74,9 +90,20 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
const appendFfmpegCommandLog = useCallback((args: string[]) => appendLastCommandsLog(getFfCommandLine('ffmpeg', args)), [appendLastCommandsLog]); const appendFfmpegCommandLog = useCallback((args: string[]) => appendLastCommandsLog(getFfCommandLine('ffmpeg', args)), [appendLastCommandsLog]);
const shouldSkipExistingFile = useCallback(async (path: string) => { const shouldSkipExistingFile = useCallback(async (path: string) => {
const skip = !enableOverwriteOutput && await pathExists(path); const fileExists = await pathExists(path);
if (skip) console.log('Not overwriting existing file', path);
return skip; // If output file exists, check that it is writable, so we can inform user if it's not (or else ffmpeg will fail with "Permission denied")
// this seems to sometimes happen on Windows, not sure why.
if (fileExists) {
try {
await access(path, W_OK);
} catch {
throw new OutputNotWritableError();
}
}
const shouldSkip = !enableOverwriteOutput && fileExists;
if (shouldSkip) console.log('Not overwriting existing file', path);
return shouldSkip;
}, [enableOverwriteOutput]); }, [enableOverwriteOutput]);
const getOutputPlaybackRateArgs = useCallback(() => (outputPlaybackRate !== 1 ? ['-itsscale', String(1 / outputPlaybackRate)] : []), [outputPlaybackRate]); const getOutputPlaybackRateArgs = useCallback(() => (outputPlaybackRate !== 1 ? ['-itsscale', String(1 / outputPlaybackRate)] : []), [outputPlaybackRate]);

View File

@ -318,6 +318,9 @@ export function isExecaError(err: unknown): err is InvariantExecaError {
return err instanceof Error && 'stdout' in err && 'stderr' in err; return err instanceof Error && 'stdout' in err && 'stderr' in err;
} }
// execa killed (aborted by user)
export const isAbortedError = (err: unknown) => isExecaError(err) && err.killed;
export const getStdioString = (stdio: string | Buffer | undefined) => (stdio instanceof Buffer ? stdio.toString('utf8') : stdio); export const getStdioString = (stdio: string | Buffer | undefined) => (stdio instanceof Buffer ? stdio.toString('utf8') : stdio);
// A bit hacky but it works, unless someone has a file called "No space left on device" ( ͡° ͜ʖ ͡°) // A bit hacky but it works, unless someone has a file called "No space left on device" ( ͡° ͜ʖ ͡°)