[desktop] Finalize zip handling (#1576)

And other fixes. Getting close to a stable desktop build.
This commit is contained in:
Manav Rathi 2024-05-01 15:42:12 +05:30 committed by GitHub
commit d30a8b8033
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 76 additions and 62 deletions

View file

@ -17,8 +17,11 @@ export const selectDirectory = async () => {
* For example, on macOS this'll open {@link dirPath} in Finder.
*/
export const openDirectory = async (dirPath: string) => {
// We need to use `path.normalize` because `shell.openPath; does not support
// POSIX path, it needs to be a platform specific path:
// https://github.com/electron/electron/issues/28831#issuecomment-826370589
const res = await shell.openPath(path.normalize(dirPath));
// shell.openPath resolves with a string containing the error message
// `shell.openPath` resolves with a string containing the error message
// corresponding to the failure if a failure occurred, otherwise "".
if (res) throw new Error(`Failed to open directory ${dirPath}: res`);
};

View file

@ -14,7 +14,7 @@ export const listZipItems = async (zipPath: string): Promise<ZipItem[]> => {
for (const entry of Object.values(entries)) {
const basename = path.basename(entry.name);
// Ignore "hidden" files (files whose names begins with a dot).
if (entry.isFile && basename.startsWith(".")) {
if (entry.isFile && !basename.startsWith(".")) {
// `entry.name` is the path within the zip.
entryNames.push(entry.name);
}

View file

@ -35,8 +35,8 @@ export const createWatcher = (mainWindow: BrowserWindow) => {
return watcher;
};
const eventData = (path: string): [string, FolderWatch] => {
path = posixPath(path);
const eventData = (platformPath: string): [string, FolderWatch] => {
const path = posixPath(platformPath);
const watch = folderWatches().find((watch) =>
path.startsWith(watch.folderPath + "/"),

View file

@ -9,15 +9,10 @@ export interface UploadStatusStore {
collectionName?: string;
/**
* Paths to regular files that are pending upload.
*
* This should generally be present, albeit empty, but it is marked optional
* in sympathy with its siblings.
*/
filePaths?: string[];
/**
* Each item is the path to a zip file and the name of an entry within it.
*
* This is marked optional since legacy stores will not have it.
*/
zipItems?: [zipPath: string, entryName: string][];
/**

View file

@ -48,7 +48,8 @@ export const registerStreamProtocol = () => {
const { host, pathname, hash } = new URL(url);
// Convert e.g. "%20" to spaces.
const path = decodeURIComponent(pathname);
const hashPath = decodeURIComponent(hash);
// `hash` begins with a "#", slice that off.
const hashPath = decodeURIComponent(hash.slice(1));
switch (host) {
case "read":
return handleRead(path);
@ -116,7 +117,6 @@ const handleReadZip = async (zipPath: string, entryName: string) => {
webReadableStreamAny as ReadableStream<Uint8Array>;
// Close the zip handle when the underlying stream closes.
// TODO(MR): Verify
stream.on("end", () => void zip.close());
return new Response(webReadableStream, {

View file

@ -9,11 +9,26 @@ import log from "../log";
export const isDev = !app.isPackaged;
/**
* Convert a file system {@link filePath} that uses the local system specific
* path separators into a path that uses POSIX file separators.
* Convert a file system {@link platformPath} that uses the local system
* specific path separators into a path that uses POSIX file separators.
*
* For all paths that we persist or pass over the IPC boundary, we always use
* POSIX paths, even on Windows.
*
* Windows recognizes both forward and backslashes. This also works with drive
* names. c:\foo\bar and c:/foo/bar are both valid.
*
* > Almost all paths passed to Windows APIs are normalized. During
* > normalization, Windows performs the following steps: ... All forward
* > slashes (/) are converted into the standard Windows separator, the back
* > slash (\).
* >
* > https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats
*/
export const posixPath = (filePath: string) =>
filePath.split(path.sep).join(path.posix.sep);
export const posixPath = (platformPath: string) =>
path.sep == path.posix.sep
? platformPath
: platformPath.split(path.sep).join(path.posix.sep);
/**
* Run a shell command asynchronously.

View file

@ -308,7 +308,7 @@ const PhotoFrame = ({
item: EnteFile,
) => {
log.info(
`[${item.id}] getSlideData called for thumbnail: ${!!item.msrc} sourceLoaded: ${item.isSourceLoaded} fetching:${fetching[item.id]}`,
`[${item.id}] getSlideData called for thumbnail: ${!!item.msrc} sourceLoaded: ${!!item.isSourceLoaded} fetching: ${!!fetching[item.id]}`,
);
if (!item.msrc) {

View file

@ -261,7 +261,9 @@ export default function Uploader({
const { collectionName, filePaths, zipItems } = pending;
log.info("Resuming pending upload", pending);
log.info(
`Resuming pending of upload of ${filePaths.length + zipItems.length} items${collectionName ? " to collection " + collectionName : ""}`,
);
isPendingDesktopUpload.current = true;
pendingDesktopUploadCollectionName.current = collectionName;
setDesktopFilePaths(filePaths);
@ -323,13 +325,26 @@ export default function Uploader({
// Trigger an upload when any of the dependencies change.
useEffect(() => {
// Re the paths:
//
// - These are not necessarily the full paths. In particular, when
// running on the browser they'll be the relative paths (at best) or
// just the file-name otherwise.
//
// - All the paths use POSIX separators. See inline comments.
const allItemAndPaths = [
// See: [Note: webkitRelativePath]. In particular, they use POSIX
// separators.
webFiles.map((f) => [f, f.webkitRelativePath ?? f.name]),
// The paths we get from the desktop app all eventually come either
// from electron.selectDirectory or electron.pathForFile, both of
// which return POSIX paths.
desktopFiles.map((fp) => [fp, fp.path]),
desktopFilePaths.map((p) => [p, p]),
// ze[1], the entry name, uses POSIX separators.
// The first path, that of the zip file itself, is POSIX like the
// other paths we get over the IPC boundary. And the second path,
// ze[1], the entry name, uses POSIX separators because that is what
// the ZIP format uses.
desktopZipItems.map((ze) => [ze, ze[1]]),
].flat() as [UploadItem, string][];
@ -792,10 +807,7 @@ async function waitAndRun(
await task();
}
const desktopFilesAndZipItems = async (
electron: Electron,
files: File[],
): Promise<{ fileAndPaths: FileAndPath[]; zipItems: ZipItem[] }> => {
const desktopFilesAndZipItems = async (electron: Electron, files: File[]) => {
const fileAndPaths: FileAndPath[] = [];
let zipItems: ZipItem[] = [];

View file

@ -167,14 +167,7 @@ function parseExifData(exifData: RawEXIFData): ParsedEXIFData {
parsedExif.imageWidth = ImageWidth;
parsedExif.imageHeight = ImageHeight;
} else {
log.error(
`Image dimension parsing failed - ImageWidth or ImageHeight is not a number ${JSON.stringify(
{
ImageWidth,
ImageHeight,
},
)}`,
);
log.warn("EXIF: Ignoring non-numeric ImageWidth or ImageHeight");
}
} else if (ExifImageWidth && ExifImageHeight) {
if (
@ -184,13 +177,8 @@ function parseExifData(exifData: RawEXIFData): ParsedEXIFData {
parsedExif.imageWidth = ExifImageWidth;
parsedExif.imageHeight = ExifImageHeight;
} else {
log.error(
`Image dimension parsing failed - ExifImageWidth or ExifImageHeight is not a number ${JSON.stringify(
{
ExifImageWidth,
ExifImageHeight,
},
)}`,
log.warn(
"EXIF: Ignoring non-numeric ExifImageWidth or ExifImageHeight",
);
}
} else if (PixelXDimension && PixelYDimension) {
@ -201,13 +189,8 @@ function parseExifData(exifData: RawEXIFData): ParsedEXIFData {
parsedExif.imageWidth = PixelXDimension;
parsedExif.imageHeight = PixelYDimension;
} else {
log.error(
`Image dimension parsing failed - PixelXDimension or PixelYDimension is not a number ${JSON.stringify(
{
PixelXDimension,
PixelYDimension,
},
)}`,
log.warn(
"EXIF: Ignoring non-numeric PixelXDimension or PixelYDimension",
);
}
}
@ -302,15 +285,13 @@ export function parseEXIFLocation(
);
return { latitude, longitude };
} catch (e) {
log.error(
`Failed to parseEXIFLocation ${JSON.stringify({
gpsLatitude,
gpsLatitudeRef,
gpsLongitude,
gpsLongitudeRef,
})}`,
e,
);
const p = {
gpsLatitude,
gpsLatitudeRef,
gpsLongitude,
gpsLongitudeRef,
};
log.error(`Failed to parse EXIF location ${JSON.stringify(p)}`, e);
return { ...NULL_LOCATION };
}
}

View file

@ -547,6 +547,9 @@ class ExportService {
isCanceled: CancellationStatus,
) {
const fs = ensureElectron().fs;
const rmdirIfExists = async (dirPath: string) => {
if (await fs.exists(dirPath)) await fs.rmdir(dirPath);
};
try {
const exportRecord = await this.getExportRecord(exportFolder);
const collectionIDPathMap =
@ -581,11 +584,11 @@ class ExportService {
);
try {
// delete the collection metadata folder
await fs.rmdir(
await rmdirIfExists(
getMetadataFolderExportPath(collectionExportPath),
);
// delete the collection folder
await fs.rmdir(collectionExportPath);
await rmdirIfExists(collectionExportPath);
} catch (e) {
await this.addCollectionExportedRecord(
exportFolder,
@ -1398,17 +1401,19 @@ const moveToTrash = async (
if (await fs.exists(filePath)) {
await fs.mkdirIfNeeded(trashDir);
const trashFilePath = await safeFileName(trashDir, fileName, fs.exists);
const trashFileName = await safeFileName(trashDir, fileName, fs.exists);
const trashFilePath = `${trashDir}/${trashFileName}`;
await fs.rename(filePath, trashFilePath);
}
if (await fs.exists(metadataFilePath)) {
await fs.mkdirIfNeeded(metadataTrashDir);
const metadataTrashFilePath = await safeFileName(
const metadataTrashFileName = await safeFileName(
metadataTrashDir,
metadataFileName,
fs.exists,
);
await fs.rename(filePath, metadataTrashFilePath);
const metadataTrashFilePath = `${metadataTrashDir}/${metadataTrashFileName}`;
await fs.rename(metadataFilePath, metadataTrashFilePath);
}
};

View file

@ -1021,7 +1021,7 @@ const withThumbnail = async (
fileTypeInfo,
);
} catch (e) {
if (e.message == CustomErrorMessage.NotAvailable) {
if (e.message.endsWith(CustomErrorMessage.NotAvailable)) {
moduleState.isNativeImageThumbnailGenerationNotAvailable = true;
} else {
log.error("Native thumbnail generation failed", e);

View file

@ -301,7 +301,8 @@ export const getRenderableImage = async (fileName: string, imageBlob: Blob) => {
const tempFile = new File([imageBlob], fileName);
const fileTypeInfo = await detectFileTypeInfo(tempFile);
log.debug(
() => `Need renderable image for ${JSON.stringify(fileTypeInfo)}`,
() =>
`Need renderable image for ${JSON.stringify({ fileName, ...fileTypeInfo })}`,
);
const { extension } = fileTypeInfo;
@ -318,7 +319,7 @@ export const getRenderableImage = async (fileName: string, imageBlob: Blob) => {
try {
return await nativeConvertToJPEG(imageBlob);
} catch (e) {
if (e.message == CustomErrorMessage.NotAvailable) {
if (e.message.endsWith(CustomErrorMessage.NotAvailable)) {
moduleState.isNativeJPEGConversionNotAvailable = true;
} else {
log.error("Native conversion to JPEG failed", e);

View file

@ -42,7 +42,7 @@ export const readStream = async (
url = new URL(`stream://read${pathOrZipItem}`);
} else {
const [zipPath, entryName] = pathOrZipItem;
url = new URL(`stream://read${zipPath}`);
url = new URL(`stream://read-zip${zipPath}`);
url.hash = entryName;
}

View file

@ -53,6 +53,8 @@ export interface Electron {
* Ask the user to select a directory on their local file system, and return
* it path.
*
* The returned path is guaranteed to use POSIX separators ('/').
*
* We don't strictly need IPC for this, we can use a hidden <input> element
* and trigger its click for the same behaviour (as we do for the
* `useFileInput` hook that we use for uploads). However, it's a bit