diff --git a/package.json b/package.json index 84abe8693..de8fbe591 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,11 @@ ] } ], + "asarUnpack": [ + "node_modules/ffmpeg-static/bin/${os}/${arch}/ffmpeg", + "node_modules/ffmpeg-static/index.js", + "node_modules/ffmpeg-static/package.json" + ], "files": [ "app/**/*", { @@ -69,6 +74,7 @@ "devDependencies": { "@sentry/cli": "^1.68.0", "@types/auto-launch": "^5.0.2", + "@types/ffmpeg-static": "^3.0.1", "@types/get-folder-size": "^2.0.0", "@types/node-fetch": "^2.6.2", "@types/semver-compare": "^1.0.1", @@ -91,12 +97,14 @@ "@sentry/electron": "^2.5.1", "@types/node": "^14.14.37", "@types/promise-fs": "^2.1.1", + "any-shell-escape": "^0.1.1", "auto-launch": "^5.0.5", "chokidar": "^3.5.3", "electron-log": "^4.3.5", "electron-reload": "^2.0.0-alpha.1", "electron-store": "^8.0.1", "electron-updater": "^4.3.8", + "ffmpeg-static": "^5.1.0", "get-folder-size": "^2.0.1", "next-electron-server": "file:./thirdparty/next-electron-server", "node-fetch": "^2.6.7", diff --git a/src/api/ffmpeg.ts b/src/api/ffmpeg.ts new file mode 100644 index 000000000..94783fa51 --- /dev/null +++ b/src/api/ffmpeg.ts @@ -0,0 +1,40 @@ +import { ipcRenderer } from 'electron'; +import { logError } from '../services/logging'; +import { ElectronFile } from '../types'; + +export async function runFFmpegCmd( + cmd: string[], + inputFile: File | ElectronFile, + outputFileName: string +) { + let inputFilePath = null; + let createdTempInputFile = null; + try { + if (!inputFile.path) { + const inputFileData = new Uint8Array(await inputFile.arrayBuffer()); + inputFilePath = await ipcRenderer.invoke( + 'write-temp-file', + inputFileData, + inputFile.name + ); + createdTempInputFile = true; + } else { + inputFilePath = inputFile.path; + } + const outputFileData = await ipcRenderer.invoke( + 'run-ffmpeg-cmd', + cmd, + inputFilePath, + outputFileName + ); + return new File([outputFileData], outputFileName); + } finally { + if (createdTempInputFile) { + try { + await ipcRenderer.invoke('remove-temp-file', inputFilePath); + } catch (e) { + logError(e, 'failed to deleteTempFile'); + } + } + } +} diff --git a/src/preload.ts b/src/preload.ts index 7fd1dfde0..d51f20e92 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -50,6 +50,7 @@ import { fixHotReloadNext12 } from './utils/preload'; import { isFolder, getDirFiles } from './api/fs'; import { convertHEIC } from './api/heicConvert'; import { setupLogging } from './utils/logging'; +import { runFFmpegCmd } from './api/ffmpeg'; fixHotReloadNext12(); setupLogging(); @@ -100,4 +101,5 @@ windowObject['ElectronAPIs'] = { skipAppVersion, getSentryUserID, getAppVersion, + runFFmpegCmd, }; diff --git a/src/services/ffmpeg.ts b/src/services/ffmpeg.ts new file mode 100644 index 000000000..104a47dcb --- /dev/null +++ b/src/services/ffmpeg.ts @@ -0,0 +1,70 @@ +import pathToFfmpeg from 'ffmpeg-static'; +const shellescape = require('any-shell-escape'); +import util from 'util'; +import log from 'electron-log'; +import { readFile, rmSync, writeFile } from 'promise-fs'; +import { logErrorSentry } from './sentry'; +import { generateTempFilePath, getTempDirPath } from '../utils/temp'; + +const execAsync = util.promisify(require('child_process').exec); + +export const INPUT_PATH_PLACEHOLDER = 'INPUT'; +export const FFMPEG_PLACEHOLDER = 'FFMPEG'; +export const OUTPUT_PATH_PLACEHOLDER = 'OUTPUT'; + +function getFFmpegStaticPath() { + return pathToFfmpeg.replace('app.asar', 'app.asar.unpacked'); +} + +export async function runFFmpegCmd( + cmd: string[], + inputFilePath: string, + outputFileName: string +) { + let tempOutputFilePath: string; + try { + tempOutputFilePath = await generateTempFilePath(outputFileName); + + cmd = cmd.map((cmdPart) => { + if (cmdPart === FFMPEG_PLACEHOLDER) { + return getFFmpegStaticPath(); + } else if (cmdPart === INPUT_PATH_PLACEHOLDER) { + return inputFilePath; + } else if (cmdPart === OUTPUT_PATH_PLACEHOLDER) { + return tempOutputFilePath; + } else { + return cmdPart; + } + }); + cmd = shellescape(cmd); + log.info('cmd', cmd); + await execAsync(cmd); + return new Uint8Array(await readFile(tempOutputFilePath)); + } catch (e) { + logErrorSentry(e, 'ffmpeg run command error'); + throw e; + } finally { + try { + rmSync(tempOutputFilePath); + } catch (e) { + logErrorSentry(e, 'failed to remove tempOutputFile'); + } + } +} + +export async function writeTempFile(fileStream: Uint8Array, fileName: string) { + const tempFilePath = await generateTempFilePath(fileName); + await writeFile(tempFilePath, fileStream); + return tempFilePath; +} + +export async function deleteTempFile(tempFilePath: string) { + const tempDirPath = await getTempDirPath(); + if (!tempFilePath.startsWith(tempDirPath)) { + logErrorSentry( + Error('not a temp file'), + 'tried to delete a non temp file' + ); + } + rmSync(tempFilePath); +} diff --git a/src/services/heicConvertor.ts b/src/services/heicConvertor.ts index 6fbc818cc..cb90bbba3 100644 --- a/src/services/heicConvertor.ts +++ b/src/services/heicConvertor.ts @@ -1,46 +1,27 @@ -import { exec, ExecException } from 'child_process'; -import { app } from 'electron'; -import { existsSync, rmSync } from 'fs'; -import path from 'path'; -import { mkdir, readFile, writeFile } from 'promise-fs'; -import { generateRandomName } from '../utils/common'; +import util from 'util'; +import { exec } from 'child_process'; + +import { rmSync } from 'fs'; +import { readFile, writeFile } from 'promise-fs'; +import { generateTempFilePath } from '../utils/temp'; import { logErrorSentry } from './sentry'; +const asyncExec = util.promisify(exec); + export async function convertHEIC( heicFileData: Uint8Array ): Promise { let tempInputFilePath: string; let tempOutputFilePath: string; try { - const tempDir = path.join(app.getPath('temp'), 'ente'); - if (!existsSync(tempDir)) { - await mkdir(tempDir); - } - const tempName = generateRandomName(10); - - tempInputFilePath = path.join(tempDir, tempName + '.heic'); - tempOutputFilePath = path.join(tempDir, tempName + '.jpeg'); + tempInputFilePath = await generateTempFilePath('.heic'); + tempOutputFilePath = await generateTempFilePath('.jpeg'); await writeFile(tempInputFilePath, heicFileData); - await new Promise((resolve, reject) => { - exec( - `sips -s format jpeg ${tempInputFilePath} --out ${tempOutputFilePath}`, - ( - error: ExecException | null, - stdout: string, - stderr: string - ) => { - if (error) { - reject(error); - } else if (stderr) { - reject(stderr); - } else { - resolve(stdout); - } - } - ); - }); + await asyncExec( + `sips -s format jpeg ${tempInputFilePath} --out ${tempOutputFilePath}` + ); const convertedFileData = new Uint8Array( await readFile(tempOutputFilePath) ); diff --git a/src/utils/common.ts b/src/utils/common.ts index 463258f46..c45cdf76a 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -1,17 +1,2 @@ import { app } from 'electron'; export const isDev = !app.isPackaged; - -const characters = - 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - -export function generateRandomName(length: number) { - let result = ''; - - const charactersLength = characters.length; - for (let i = 0; i < length; i++) { - result += characters.charAt( - Math.floor(Math.random() * charactersLength) - ); - } - return result; -} diff --git a/src/utils/ipcComms.ts b/src/utils/ipcComms.ts index 9997b612d..daa92f6c2 100644 --- a/src/utils/ipcComms.ts +++ b/src/utils/ipcComms.ts @@ -20,6 +20,11 @@ import { skipAppVersion, updateAndRestart, } from '../services/appUpdater'; +import { + deleteTempFile, + runFFmpegCmd, + writeTempFile, +} from '../services/ffmpeg'; export default function setupIpcComs( tray: Tray, @@ -125,4 +130,20 @@ export default function setupIpcComs( ipcMain.handle('get-app-version', () => { return getAppVersion(); }); + + ipcMain.handle( + 'run-ffmpeg-cmd', + (_, cmd, inputFilePath, outputFileName) => { + return runFFmpegCmd(cmd, inputFilePath, outputFileName); + } + ); + ipcMain.handle( + 'write-temp-file', + (_, fileStream: Uint8Array, fileName: string) => { + return writeTempFile(fileStream, fileName); + } + ); + ipcMain.handle('remove-temp-file', (_, tempFilePath: string) => { + return deleteTempFile(tempFilePath); + }); } diff --git a/src/utils/temp.ts b/src/utils/temp.ts new file mode 100644 index 000000000..838d3692f --- /dev/null +++ b/src/utils/temp.ts @@ -0,0 +1,38 @@ +import { app } from 'electron'; +import path from 'path'; +import { existsSync, mkdir } from 'promise-fs'; + +const ENTE_TEMP_DIRECTORY = 'ente'; + +const CHARACTERS = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + +export async function getTempDirPath() { + const tempDirPath = path.join(app.getPath('temp'), ENTE_TEMP_DIRECTORY); + if (!existsSync(tempDirPath)) { + await mkdir(tempDirPath); + } + return tempDirPath; +} + +function generateTempName(length: number) { + let result = ''; + + const charactersLength = CHARACTERS.length; + for (let i = 0; i < length; i++) { + result += CHARACTERS.charAt( + Math.floor(Math.random() * charactersLength) + ); + } + return result; +} + +export async function generateTempFilePath(formatSuffix: string) { + const tempDirPath = await getTempDirPath(); + const namePrefix = generateTempName(10); + const tempFilePath = path.join( + tempDirPath, + namePrefix + '-' + formatSuffix + ); + return tempFilePath; +} diff --git a/ui b/ui index c746bf893..6320f0295 160000 --- a/ui +++ b/ui @@ -1 +1 @@ -Subproject commit c746bf89353c8dd303778214d16434faeffce556 +Subproject commit 6320f0295d811570adad53a32ce94beb912fa9f9 diff --git a/yarn.lock b/yarn.lock index 285454640..90bad334b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -35,6 +35,16 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@derhuerst/http-basic@^8.2.0": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@derhuerst/http-basic/-/http-basic-8.2.4.tgz#d021ebb8f65d54bea681ae6f4a8733ce89e7f59b" + integrity sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw== + dependencies: + caseless "^0.12.0" + concat-stream "^2.0.0" + http-response-object "^3.0.1" + parse-cache-control "^1.0.1" + "@develar/schema-utils@~2.6.5": version "2.6.5" resolved "https://registry.yarnpkg.com/@develar/schema-utils/-/schema-utils-2.6.5.tgz#3ece22c5838402419a6e0425f85742b961d9b6c6" @@ -277,6 +287,11 @@ dependencies: "@types/ms" "*" +"@types/ffmpeg-static@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/ffmpeg-static/-/ffmpeg-static-3.0.1.tgz#1003f003624bcd2f569b56185a62dcbacd935c39" + integrity sha512-hEJdQMv/g1olk9qTiWqh23BfbKsDKE6Tc7DilNJWF1MgZsU9fYOPKrgQ448vfT7aP2Yt5re9vgJDVv9TXEoTyQ== + "@types/fs-extra@^9.0.11": version "9.0.13" resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.13.tgz#7594fbae04fe7f1918ce8b3d213f74ff44ac1f45" @@ -325,6 +340,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.3.tgz#463fc47f13ec0688a33aec75d078a0541a447199" integrity sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ== +"@types/node@^10.0.3": + version "10.17.60" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b" + integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== + "@types/node@^14.14.37", "@types/node@^14.6.2": version "14.18.21" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.21.tgz#0155ee46f6be28b2ff0342ca1a9b9fd4468bef41" @@ -567,6 +587,11 @@ ansi-styles@^6.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.1.0.tgz#87313c102b8118abd57371afab34618bf7350ed3" integrity sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ== +any-shell-escape@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/any-shell-escape/-/any-shell-escape-0.1.1.tgz#d55ab972244c71a9a5e1ab0879f30bf110806959" + integrity sha512-36j4l5HVkboyRhIWgtMh1I9i8LTdFqVwDEHy1cp+QioJyKgAUG40X0W8s7jakWRta/Sjvm8mUG1fU6Tj8mWagQ== + anymatch@~3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" @@ -900,7 +925,7 @@ camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -caseless@~0.12.0: +caseless@^0.12.0, caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== @@ -1089,6 +1114,16 @@ concat-stream@^1.6.2: readable-stream "^2.2.2" typedarray "^0.0.6" +concat-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1" + integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.0.2" + typedarray "^0.0.6" + concurrently@^7.0.0: version "7.2.2" resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-7.2.2.tgz#4ad4a4dfd3945f668d727379de2a29502e6a531c" @@ -1778,6 +1813,16 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" +ffmpeg-static@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ffmpeg-static/-/ffmpeg-static-5.1.0.tgz#133500f4566570c5a0e96795152b0526d8c936ad" + integrity sha512-eEWOiGdbf7HKPeJI5PoJ0oCwkL0hckL2JdS4JOuB/gUETppwkEpq8nF0+e6VEQnDCo/iuoipbTUsn9QJmtpNkg== + dependencies: + "@derhuerst/http-basic" "^8.2.0" + env-paths "^2.2.0" + https-proxy-agent "^5.0.0" + progress "^2.0.3" + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -2169,6 +2214,13 @@ http-proxy-agent@^5.0.0: agent-base "6" debug "4" +http-response-object@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/http-response-object/-/http-response-object-3.0.2.tgz#7f435bb210454e4360d074ef1f989d5ea8aa9810" + integrity sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA== + dependencies: + "@types/node" "^10.0.3" + http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -2995,6 +3047,11 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" +parse-cache-control@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parse-cache-control/-/parse-cache-control-1.0.1.tgz#8eeab3e54fa56920fe16ba38f77fa21aacc2d74e" + integrity sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg== + parse-json@^5.0.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" @@ -3228,6 +3285,15 @@ readable-stream@^2.0.6, readable-stream@^2.2.2: string_decoder "~1.1.1" util-deprecate "~1.0.1" +readable-stream@^3.0.2: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + readable-stream@~1.1.9: version "1.1.14" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" @@ -3372,7 +3438,7 @@ rxjs@^7.0.0, rxjs@^7.5.5: dependencies: tslib "^2.1.0" -safe-buffer@^5.0.1, safe-buffer@^5.1.2: +safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -3616,6 +3682,13 @@ string-width@^5.0.0: emoji-regex "^9.2.2" strip-ansi "^7.0.1" +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" @@ -3963,7 +4036,7 @@ utf8-byte-length@^1.0.1: resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61" integrity sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA== -util-deprecate@~1.0.1: +util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==