mirror of
https://github.com/mifi/lossless-cut.git
synced 2024-11-21 18:02:35 +01:00
check if output file is writable before exporting
if it exists so we can inform user
This commit is contained in:
parent
e7f2d39d36
commit
6c9b1ba708
@ -17,7 +17,7 @@ import { SweetAlertOptions } from 'sweetalert2';
|
||||
import theme from './theme';
|
||||
import useTimelineScroll from './hooks/useTimelineScroll';
|
||||
import useUserSettingsRoot from './hooks/useUserSettingsRoot';
|
||||
import useFfmpegOperations from './hooks/useFfmpegOperations';
|
||||
import useFfmpegOperations, { OutputNotWritableError } from './hooks/useFfmpegOperations';
|
||||
import useKeyframes from './hooks/useKeyframes';
|
||||
import useWaveform from './hooks/useWaveform';
|
||||
import useKeyboard from './hooks/useKeyboard';
|
||||
@ -71,12 +71,13 @@ import {
|
||||
calcShouldShowWaveform, calcShouldShowKeyframes, mediaSourceQualities, isExecaError, getStdioString,
|
||||
isMuxNotSupported,
|
||||
getDownloadMediaOutPath,
|
||||
isAbortedError,
|
||||
} from './util';
|
||||
import { toast, errorToast, showPlaybackFailedMessage } from './swal';
|
||||
import { adjustRate } from './util/rate-calculator';
|
||||
import { askExtractFramesAsImages } from './dialogs/extractFrames';
|
||||
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 { fallbackLng } from './i18n';
|
||||
import { findSegmentsAtCursor, sortSegments, convertSegmentsToChapters, hasAnySegmentOverlap, isDurationValid, playOnlyCurrentSegment, getSegmentTags } from './segments';
|
||||
@ -863,16 +864,11 @@ function App() {
|
||||
openConcatFinishedToast({ filePath: outPath, notices, warnings });
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof DirectoryAccessDeclinedError || isAbortedError(err)) return;
|
||||
|
||||
showOsNotification(i18n.t('Failed to merge'));
|
||||
|
||||
if (err instanceof DirectoryAccessDeclinedError) return;
|
||||
|
||||
if (isExecaError(err)) {
|
||||
if (err.killed) {
|
||||
// assume execa killed (aborted by user)
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('stdout:', getStdioString(err.stdout));
|
||||
console.error('stderr:', getStdioString(err.stderr));
|
||||
|
||||
@ -884,12 +880,15 @@ function App() {
|
||||
showMuxNotSupported();
|
||||
return;
|
||||
}
|
||||
const reportState = { includeAllStreams, streams, outFormat, outFileName, segmentsToChapters };
|
||||
handleConcatFailed(err, reportState);
|
||||
}
|
||||
|
||||
if (err instanceof OutputNotWritableError) {
|
||||
showOutputNotWritable();
|
||||
return;
|
||||
}
|
||||
|
||||
handleError(err);
|
||||
const reportState = { includeAllStreams, streams, outFormat, outFileName, segmentsToChapters };
|
||||
handleConcatFailed(err, reportState);
|
||||
} finally {
|
||||
setWorking(undefined);
|
||||
setCutProgress(undefined);
|
||||
@ -1105,17 +1104,14 @@ function App() {
|
||||
|
||||
if (cleanupChoices.cleanupAfterExport) await cleanupFilesWithDialog();
|
||||
} catch (err) {
|
||||
if (isExecaError(err)) {
|
||||
if (err.killed) {
|
||||
// assume execa killed (aborted by user)
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('stdout:', getStdioString(err.stdout));
|
||||
console.error('stderr:', getStdioString(err.stderr));
|
||||
if (isAbortedError(err)) return;
|
||||
|
||||
showOsNotification(i18n.t('Failed to export'));
|
||||
|
||||
if (isExecaError(err)) {
|
||||
console.log('stdout:', getStdioString(err.stdout));
|
||||
console.error('stderr:', getStdioString(err.stderr));
|
||||
|
||||
if (isOutOfSpaceError(err)) {
|
||||
showDiskFull();
|
||||
return;
|
||||
@ -1124,12 +1120,14 @@ function App() {
|
||||
showMuxNotSupported();
|
||||
return;
|
||||
}
|
||||
handleExportFailed(err);
|
||||
}
|
||||
|
||||
if (err instanceof OutputNotWritableError) {
|
||||
showOutputNotWritable();
|
||||
return;
|
||||
}
|
||||
|
||||
showOsNotification(i18n.t('Failed to export'));
|
||||
handleError(err);
|
||||
handleExportFailed(err);
|
||||
} finally {
|
||||
setWorking(undefined);
|
||||
setCutProgress(undefined);
|
||||
@ -1258,7 +1256,7 @@ function App() {
|
||||
|
||||
// then try to open project from source file dir
|
||||
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
|
||||
if (await exists(sameDirEdlFilePath)) {
|
||||
// Ok, the file exists. now we have to ask the user, because we need to read that 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() {
|
||||
await Swal.fire({
|
||||
icon: 'warning',
|
||||
|
@ -14,8 +14,15 @@ import { AvoidNegativeTs, Html5ifyMode } from '../../../../types';
|
||||
import { AllFilesMeta, Chapter, CopyfileStreams, CustomTagsByFile, ParamsByStreamId, SegmentToExport } from '../types';
|
||||
|
||||
const { join, resolve, dirname } = window.require('path');
|
||||
const { pathExists } = window.require('fs-extra');
|
||||
const { writeFile, mkdir } = window.require('fs/promises');
|
||||
const { writeFile, mkdir, access, constants: { F_OK, W_OK } } = window.require('fs/promises');
|
||||
|
||||
|
||||
export class OutputNotWritableError extends Error {
|
||||
constructor() {
|
||||
super();
|
||||
this.name = 'OutputNotWritableError';
|
||||
}
|
||||
}
|
||||
|
||||
async function writeChaptersFfmetadata(outDir: string, chapters: Chapter[] | 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 });
|
||||
}
|
||||
|
||||
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 }: {
|
||||
filePath: string | 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 shouldSkipExistingFile = useCallback(async (path: string) => {
|
||||
const skip = !enableOverwriteOutput && await pathExists(path);
|
||||
if (skip) console.log('Not overwriting existing file', path);
|
||||
return skip;
|
||||
const fileExists = await pathExists(path);
|
||||
|
||||
// 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]);
|
||||
|
||||
const getOutputPlaybackRateArgs = useCallback(() => (outputPlaybackRate !== 1 ? ['-itsscale', String(1 / outputPlaybackRate)] : []), [outputPlaybackRate]);
|
||||
|
@ -318,6 +318,9 @@ export function isExecaError(err: unknown): err is InvariantExecaError {
|
||||
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);
|
||||
|
||||
// A bit hacky but it works, unless someone has a file called "No space left on device" ( ͡° ͜ʖ ͡°)
|
||||
|
Loading…
Reference in New Issue
Block a user