Merge remote-tracking branch 'origin/main' into mobile-widgetsimproved

This commit is contained in:
Prateek Sunal 2024-04-15 21:43:52 +05:30
commit 0956e3ccc4
26 changed files with 380 additions and 281 deletions

View file

@ -9,7 +9,7 @@
* https://www.electronjs.org/docs/latest/tutorial/process-model#the-main-process
*/
import { nativeImage } from "electron";
import { app, BrowserWindow, Menu, Tray } from "electron/main";
import { app, BrowserWindow, Menu, protocol, Tray } from "electron/main";
import serveNextAt from "next-electron-server";
import { existsSync } from "node:fs";
import fs from "node:fs/promises";
@ -27,6 +27,7 @@ import { setupAutoUpdater } from "./main/services/app-update";
import autoLauncher from "./main/services/autoLauncher";
import { initWatcher } from "./main/services/chokidar";
import { userPreferences } from "./main/stores/user-preferences";
import { registerStreamProtocol } from "./main/stream";
import { isDev } from "./main/util";
/**
@ -58,6 +59,21 @@ export const allowWindowClose = (): void => {
shouldAllowWindowClose = true;
};
/**
* Log a standard startup banner.
*
* This helps us identify app starts and other environment details in the logs.
*/
const logStartupBanner = () => {
const version = isDev ? "dev" : app.getVersion();
log.info(`Starting ente-photos-desktop ${version}`);
const platform = process.platform;
const osRelease = os.release();
const systemVersion = process.getSystemVersion();
log.info("Running on", { platform, osRelease, systemVersion });
};
/**
* next-electron-server allows up to directly use the output of `next build` in
* production mode and `next dev` in development mode, whilst keeping the rest
@ -74,18 +90,57 @@ export const allowWindowClose = (): void => {
const setupRendererServer = () => serveNextAt(rendererURL);
/**
* Log a standard startup banner.
* Register privileged schemes.
*
* This helps us identify app starts and other environment details in the logs.
* We have two privileged schemes:
*
* 1. "ente", used for serving our web app (@see {@link setupRendererServer}).
*
* 2. "stream", used for streaming IPC (@see {@link registerStreamProtocol}).
*
* Both of these need some privileges, however, the documentation for Electron's
* [registerSchemesAsPrivileged](https://www.electronjs.org/docs/latest/api/protocol)
* says:
*
* > This method ... can be called only once.
*
* The library we use for the "ente" scheme, next-electron-server, already calls
* it once when we invoke {@link setupRendererServer}.
*
* In practice calling it multiple times just causes the values to be
* overwritten, and the last call wins. So we don't need to modify
* next-electron-server to prevent it from calling registerSchemesAsPrivileged.
* Instead, we (a) repeat what next-electron-server had done here, and (b)
* ensure that we're called after {@link setupRendererServer}.
*/
const logStartupBanner = () => {
const version = isDev ? "dev" : app.getVersion();
log.info(`Starting ente-photos-desktop ${version}`);
const registerPrivilegedSchemes = () => {
protocol.registerSchemesAsPrivileged([
{
// Taken verbatim from next-electron-server's code (index.js)
scheme: "ente",
privileges: {
standard: true,
secure: true,
allowServiceWorkers: true,
supportFetchAPI: true,
corsEnabled: true,
},
},
{
scheme: "stream",
privileges: {
// TODO(MR): Remove the commented bits if we don't end up
// needing them by the time the IPC refactoring is done.
const platform = process.platform;
const osRelease = os.release();
const systemVersion = process.getSystemVersion();
log.info("Running on", { platform, osRelease, systemVersion });
// Prevent the insecure origin issues when fetching this
// secure: true,
// Allow the web fetch API in the renderer to use this scheme.
supportFetchAPI: true,
// Allow it to be used with video tags.
// stream: true,
},
},
]);
};
/**
@ -251,8 +306,10 @@ const main = () => {
let mainWindow: BrowserWindow | undefined;
initLogging();
setupRendererServer();
logStartupBanner();
// The order of the next two calls is important
setupRendererServer();
registerPrivilegedSchemes();
increaseDiskCache();
app.on("second-instance", () => {
@ -269,11 +326,11 @@ const main = () => {
// Note that some Electron APIs can only be used after this event occurs.
app.on("ready", async () => {
mainWindow = await createMainWindow();
const watcher = initWatcher(mainWindow);
setupTrayItem(mainWindow);
Menu.setApplicationMenu(await createApplicationMenu(mainWindow));
setupTrayItem(mainWindow);
attachIPCHandlers();
attachFSWatchIPCHandlers(watcher);
attachFSWatchIPCHandlers(initWatcher(mainWindow));
registerStreamProtocol();
if (!isDev) setupAutoUpdater(mainWindow);
handleDownloads(mainWindow);
handleExternalLinks(mainWindow);

View file

@ -1,9 +1,8 @@
/**
* @file file system related functions exposed over the context bridge.
*/
import { createWriteStream, existsSync } from "node:fs";
import { existsSync } from "node:fs";
import fs from "node:fs/promises";
import { Readable } from "node:stream";
export const fsExists = (path: string) => existsSync(path);
@ -17,78 +16,13 @@ export const fsRmdir = (path: string) => fs.rmdir(path);
export const fsRm = (path: string) => fs.rm(path);
/**
* Write a (web) ReadableStream to a file at the given {@link filePath}.
*
* The returned promise resolves when the write completes.
*
* @param filePath The local filesystem path where the file should be written.
* @param readableStream A [web
* ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)
*/
export const writeStream = (filePath: string, readableStream: ReadableStream) =>
writeNodeStream(filePath, convertWebReadableStreamToNode(readableStream));
export const fsReadTextFile = async (filePath: string) =>
fs.readFile(filePath, "utf-8");
/**
* Convert a Web ReadableStream into a Node.js ReadableStream
*
* This can be used to, for example, write a ReadableStream obtained via
* `net.fetch` into a file using the Node.js `fs` APIs
*/
const convertWebReadableStreamToNode = (readableStream: ReadableStream) => {
const reader = readableStream.getReader();
const rs = new Readable();
rs._read = async () => {
try {
const result = await reader.read();
if (!result.done) {
rs.push(Buffer.from(result.value));
} else {
rs.push(null);
return;
}
} catch (e) {
rs.emit("error", e);
}
};
return rs;
};
const writeNodeStream = async (
filePath: string,
fileStream: NodeJS.ReadableStream,
) => {
const writeable = createWriteStream(filePath);
fileStream.on("error", (error) => {
writeable.destroy(error); // Close the writable stream with an error
});
fileStream.pipe(writeable);
await new Promise((resolve, reject) => {
writeable.on("finish", resolve);
writeable.on("error", async (e: unknown) => {
if (existsSync(filePath)) {
await fs.unlink(filePath);
}
reject(e);
});
});
};
/* TODO: Audit below this */
export const saveStreamToDisk = writeStream;
export const saveFileToDisk = (path: string, contents: string) =>
export const fsWriteFile = (path: string, contents: string) =>
fs.writeFile(path, contents);
export const readTextFile = async (filePath: string) =>
fs.readFile(filePath, "utf-8");
/* TODO: Audit below this */
export const isFolder = async (dirPath: string) => {
if (!existsSync(dirPath)) return false;

View file

@ -20,13 +20,12 @@ import {
import {
fsExists,
fsMkdirIfNeeded,
fsReadTextFile,
fsRename,
fsRm,
fsRmdir,
fsWriteFile,
isFolder,
readTextFile,
saveFileToDisk,
saveStreamToDisk,
} from "./fs";
import { logToDisk } from "./log";
import {
@ -113,6 +112,26 @@ export const attachIPCHandlers = () => {
ipcMain.on("skipAppUpdate", (_, version) => skipAppUpdate(version));
// - FS
ipcMain.handle("fsExists", (_, path) => fsExists(path));
ipcMain.handle("fsRename", (_, oldPath: string, newPath: string) =>
fsRename(oldPath, newPath),
);
ipcMain.handle("fsMkdirIfNeeded", (_, dirPath) => fsMkdirIfNeeded(dirPath));
ipcMain.handle("fsRmdir", (_, path: string) => fsRmdir(path));
ipcMain.handle("fsRm", (_, path: string) => fsRm(path));
ipcMain.handle("fsReadTextFile", (_, path: string) => fsReadTextFile(path));
ipcMain.handle("fsWriteFile", (_, path: string, contents: string) =>
fsWriteFile(path, contents),
);
// - Conversion
ipcMain.handle("convertToJPEG", (_, fileData, filename) =>
@ -164,34 +183,8 @@ export const attachIPCHandlers = () => {
ipcMain.handle("showUploadZipDialog", () => showUploadZipDialog());
// - FS
ipcMain.handle("fsExists", (_, path) => fsExists(path));
ipcMain.handle("fsRename", (_, oldPath: string, newPath: string) =>
fsRename(oldPath, newPath),
);
ipcMain.handle("fsMkdirIfNeeded", (_, dirPath) => fsMkdirIfNeeded(dirPath));
ipcMain.handle("fsRmdir", (_, path: string) => fsRmdir(path));
ipcMain.handle("fsRm", (_, path: string) => fsRm(path));
// - FS Legacy
ipcMain.handle(
"saveStreamToDisk",
(_, path: string, fileStream: ReadableStream) =>
saveStreamToDisk(path, fileStream),
);
ipcMain.handle("saveFileToDisk", (_, path: string, contents: string) =>
saveFileToDisk(path, contents),
);
ipcMain.handle("readTextFile", (_, path: string) => readTextFile(path));
ipcMain.handle("isFolder", (_, dirPath: string) => isFolder(dirPath));
// - Upload

View file

@ -2,7 +2,7 @@ import pathToFfmpeg from "ffmpeg-static";
import { existsSync } from "node:fs";
import fs from "node:fs/promises";
import { ElectronFile } from "../../types/ipc";
import { writeStream } from "../fs";
import { writeStream } from "../stream";
import log from "../log";
import { generateTempFilePath, getTempDirPath } from "../temp";
import { execAsync } from "../util";

View file

@ -2,7 +2,7 @@ import { existsSync } from "fs";
import fs from "node:fs/promises";
import path from "path";
import { CustomErrors, ElectronFile } from "../../types/ipc";
import { writeStream } from "../fs";
import { writeStream } from "../stream";
import log from "../log";
import { isPlatform } from "../platform";
import { generateTempFilePath } from "../temp";

View file

@ -11,7 +11,7 @@ import fs from "node:fs/promises";
import * as ort from "onnxruntime-node";
import Tokenizer from "../../thirdparty/clip-bpe-ts/mod";
import { CustomErrors } from "../../types/ipc";
import { writeStream } from "../fs";
import { writeStream } from "../stream";
import log from "../log";
import { generateTempFilePath } from "../temp";
import { deleteTempFile } from "./ffmpeg";

View file

@ -15,7 +15,7 @@ import { existsSync } from "fs";
import fs from "node:fs/promises";
import path from "node:path";
import * as ort from "onnxruntime-node";
import { writeStream } from "../fs";
import { writeStream } from "../stream";
import log from "../log";
/**

116
desktop/src/main/stream.ts Normal file
View file

@ -0,0 +1,116 @@
/**
* @file stream data to-from renderer using a custom protocol handler.
*/
import { protocol } from "electron/main";
import { createWriteStream, existsSync } from "node:fs";
import fs from "node:fs/promises";
import { Readable } from "node:stream";
import log from "./log";
/**
* Register a protocol handler that we use for streaming large files between the
* main process (node) and the renderer process (browser) layer.
*
* [Note: IPC streams]
*
* When running without node integration, there is no direct way to pass streams
* across IPC. And passing the entire contents of the file is not feasible for
* large video files because of the memory pressure the copying would entail.
*
* As an alternative, we register a custom protocol handler that can provided a
* bi-directional stream. The renderer can stream data to the node side by
* streaming the request. The node side can stream to the renderer side by
* streaming the response.
*
* See also: [Note: Transferring large amount of data over IPC]
*
* Depends on {@link registerPrivilegedSchemes}.
*/
export const registerStreamProtocol = () => {
protocol.handle("stream", async (request: Request) => {
const url = request.url;
const { host, pathname } = new URL(url);
// Convert e.g. "%20" to spaces.
const path = decodeURIComponent(pathname);
switch (host) {
/* stream://write/path/to/file */
/* host-pathname----- */
case "write":
try {
await writeStream(path, request.body);
return new Response("", { status: 200 });
} catch (e) {
log.error(`Failed to write stream for ${url}`, e);
return new Response(
`Failed to write stream: ${e.message}`,
{ status: 500 },
);
}
default:
return new Response("", { status: 404 });
}
});
};
/**
* Write a (web) ReadableStream to a file at the given {@link filePath}.
*
* The returned promise resolves when the write completes.
*
* @param filePath The local filesystem path where the file should be written.
* @param readableStream A [web
* ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)
*/
export const writeStream = (filePath: string, readableStream: ReadableStream) =>
writeNodeStream(filePath, convertWebReadableStreamToNode(readableStream));
/**
* Convert a Web ReadableStream into a Node.js ReadableStream
*
* This can be used to, for example, write a ReadableStream obtained via
* `net.fetch` into a file using the Node.js `fs` APIs
*/
const convertWebReadableStreamToNode = (readableStream: ReadableStream) => {
const reader = readableStream.getReader();
const rs = new Readable();
rs._read = async () => {
try {
const result = await reader.read();
if (!result.done) {
rs.push(Buffer.from(result.value));
} else {
rs.push(null);
return;
}
} catch (e) {
rs.emit("error", e);
}
};
return rs;
};
const writeNodeStream = async (
filePath: string,
fileStream: NodeJS.ReadableStream,
) => {
const writeable = createWriteStream(filePath);
fileStream.on("error", (error) => {
writeable.destroy(error); // Close the writable stream with an error
});
fileStream.pipe(writeable);
await new Promise((resolve, reject) => {
writeable.on("finish", resolve);
writeable.on("error", async (e: unknown) => {
if (existsSync(filePath)) {
await fs.unlink(filePath);
}
reject(e);
});
});
};

View file

@ -96,6 +96,8 @@ const skipAppUpdate = (version: string) => {
ipcRenderer.send("skipAppUpdate", version);
};
// - FS
const fsExists = (path: string): Promise<boolean> =>
ipcRenderer.invoke("fsExists", path);
@ -110,6 +112,12 @@ const fsRmdir = (path: string): Promise<void> =>
const fsRm = (path: string): Promise<void> => ipcRenderer.invoke("fsRm", path);
const fsReadTextFile = (path: string): Promise<string> =>
ipcRenderer.invoke("fsReadTextFile", path);
const fsWriteFile = (path: string, contents: string): Promise<void> =>
ipcRenderer.invoke("fsWriteFile", path, contents);
// - AUDIT below this
// - Conversion
@ -229,17 +237,6 @@ const updateWatchMappingIgnoredFiles = (
// - FS Legacy
const saveStreamToDisk = (
path: string,
fileStream: ReadableStream,
): Promise<void> => ipcRenderer.invoke("saveStreamToDisk", path, fileStream);
const saveFileToDisk = (path: string, contents: string): Promise<void> =>
ipcRenderer.invoke("saveFileToDisk", path, contents);
const readTextFile = (path: string): Promise<string> =>
ipcRenderer.invoke("readTextFile", path);
const isFolder = (dirPath: string): Promise<boolean> =>
ipcRenderer.invoke("isFolder", dirPath);
@ -298,7 +295,8 @@ const getDirFiles = (dirPath: string): Promise<ElectronFile[]> =>
// https://www.electronjs.org/docs/latest/api/context-bridge#methods
//
// The copy itself is relatively fast, but the problem with transfering large
// amounts of data is potentially running out of memory during the copy.
// amounts of data is potentially running out of memory during the copy. For an
// alternative, see [Note: IPC streams].
contextBridge.exposeInMainWorld("electron", {
// - General
appVersion,
@ -316,6 +314,17 @@ contextBridge.exposeInMainWorld("electron", {
updateOnNextRestart,
skipAppUpdate,
// - FS
fs: {
exists: fsExists,
rename: fsRename,
mkdirIfNeeded: fsMkdirIfNeeded,
rmdir: fsRmdir,
rm: fsRm,
readTextFile: fsReadTextFile,
writeFile: fsWriteFile,
},
// - Conversion
convertToJPEG,
generateImageThumbnail,
@ -341,20 +350,8 @@ contextBridge.exposeInMainWorld("electron", {
updateWatchMappingSyncedFiles,
updateWatchMappingIgnoredFiles,
// - FS
fs: {
exists: fsExists,
rename: fsRename,
mkdirIfNeeded: fsMkdirIfNeeded,
rmdir: fsRmdir,
rm: fsRm,
},
// - FS legacy
// TODO: Move these into fs + document + rename if needed
saveStreamToDisk,
saveFileToDisk,
readTextFile,
isFolder,
// - Upload

View file

@ -22,6 +22,7 @@ linter:
- use_key_in_widget_constructors
- cancel_subscriptions
- avoid_empty_else
- exhaustive_cases
@ -59,6 +60,7 @@ analyzer:
prefer_final_locals: warning
unnecessary_const: error
cancel_subscriptions: error
unrelated_type_equality_checks: error
unawaited_futures: warning # convert to warning after fixing existing issues

View file

@ -3,12 +3,9 @@ PODS:
- Flutter
- battery_info (0.0.1):
- Flutter
- bonsoir_darwin (3.0.0):
- Flutter
- FlutterMacOS
- connectivity_plus (0.0.1):
- Flutter
- ReachabilitySwift
- FlutterMacOS
- device_info_plus (0.0.1):
- Flutter
- file_saver (0.0.1):
@ -171,7 +168,6 @@ PODS:
- Flutter
- FlutterMacOS
- PromisesObjC (2.4.0)
- ReachabilitySwift (5.2.1)
- receive_sharing_intent (1.6.8):
- Flutter
- screen_brightness_ios (0.1.0):
@ -231,8 +227,7 @@ PODS:
DEPENDENCIES:
- background_fetch (from `.symlinks/plugins/background_fetch/ios`)
- battery_info (from `.symlinks/plugins/battery_info/ios`)
- bonsoir_darwin (from `.symlinks/plugins/bonsoir_darwin/darwin`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_saver (from `.symlinks/plugins/file_saver/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
@ -296,7 +291,6 @@ SPEC REPOS:
- onnxruntime-objc
- OrderedSet
- PromisesObjC
- ReachabilitySwift
- SDWebImage
- SDWebImageWebPCoder
- Sentry
@ -309,10 +303,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/background_fetch/ios"
battery_info:
:path: ".symlinks/plugins/battery_info/ios"
bonsoir_darwin:
:path: ".symlinks/plugins/bonsoir_darwin/darwin"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
:path: ".symlinks/plugins/connectivity_plus/darwin"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
file_saver:
@ -409,8 +401,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
background_fetch: 2319bf7e18237b4b269430b7f14d177c0df09c5a
battery_info: 09f5c9ee65394f2291c8c6227bedff345b8a730c
bonsoir_darwin: 127bdc632fdc154ae2f277a4d5c86a6212bc75be
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
Firebase: 797fd7297b7e1be954432743a0b3f90038e45a71
@ -458,7 +449,6 @@ SPEC CHECKSUMS:
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
ReachabilitySwift: 5ae15e16814b5f9ef568963fb2c87aeb49158c66
receive_sharing_intent: 6837b01768e567fe8562182397bf43d63d8c6437
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
SDWebImage: 40b0b4053e36c660a764958bff99eed16610acbb

View file

@ -285,7 +285,6 @@
"${BUILT_PRODUCTS_DIR}/Mantle/Mantle.framework",
"${BUILT_PRODUCTS_DIR}/OrderedSet/OrderedSet.framework",
"${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework",
"${BUILT_PRODUCTS_DIR}/ReachabilitySwift/Reachability.framework",
"${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework",
"${BUILT_PRODUCTS_DIR}/SDWebImageWebPCoder/SDWebImageWebPCoder.framework",
"${BUILT_PRODUCTS_DIR}/Sentry/Sentry.framework",
@ -293,7 +292,6 @@
"${BUILT_PRODUCTS_DIR}/Toast/Toast.framework",
"${BUILT_PRODUCTS_DIR}/background_fetch/background_fetch.framework",
"${BUILT_PRODUCTS_DIR}/battery_info/battery_info.framework",
"${BUILT_PRODUCTS_DIR}/bonsoir_darwin/bonsoir_darwin.framework",
"${BUILT_PRODUCTS_DIR}/connectivity_plus/connectivity_plus.framework",
"${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework",
"${BUILT_PRODUCTS_DIR}/file_saver/file_saver.framework",
@ -369,7 +367,6 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Mantle.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OrderedSet.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reachability.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImageWebPCoder.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sentry.framework",
@ -377,7 +374,6 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Toast.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/background_fetch.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/battery_info.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/bonsoir_darwin.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/connectivity_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_saver.framework",

View file

@ -85,13 +85,24 @@ class EnteFile {
static int parseFileCreationTime(String? fileTitle, AssetEntity asset) {
int creationTime = asset.createDateTime.microsecondsSinceEpoch;
final int modificationTime = asset.modifiedDateTime.microsecondsSinceEpoch;
if (creationTime >= jan011981Time) {
// assuming that fileSystem is returning correct creationTime.
// During upload, this might get overridden with exif Creation time
// When the assetModifiedTime is less than creationTime, than just use
// that as creationTime. This is to handle cases where file might be
// copied to the fileSystem from somewhere else See #https://superuser.com/a/1091147
if (modificationTime >= jan011981Time &&
modificationTime < creationTime) {
_logger.info(
'LocalID: ${asset.id} modification time is less than creation time. Using modification time as creation time',
);
creationTime = modificationTime;
}
return creationTime;
} else {
if (asset.modifiedDateTime.microsecondsSinceEpoch >= jan011981Time) {
creationTime = asset.modifiedDateTime.microsecondsSinceEpoch;
if (modificationTime >= jan011981Time) {
creationTime = modificationTime;
} else {
creationTime = DateTime.now().toUtc().microsecondsSinceEpoch;
}
@ -106,7 +117,6 @@ class EnteFile {
// ignore
}
}
return creationTime;
}

View file

@ -4,7 +4,6 @@ import "dart:io";
import "package:connectivity_plus/connectivity_plus.dart";
import "package:logging/logging.dart";
import "package:photos/core/errors.dart";
import "package:photos/core/event_bus.dart";
import "package:photos/events/event.dart";
import "package:photos/services/remote_assets_service.dart";
@ -23,7 +22,7 @@ abstract class MLFramework {
MLFramework(this.shouldDownloadOverMobileData) {
Connectivity()
.onConnectivityChanged
.listen((ConnectivityResult result) async {
.listen((List<ConnectivityResult> result) async {
_logger.info("Connectivity changed to $result");
if (_state == InitializationState.waitingForNetwork &&
await _canDownload()) {
@ -135,9 +134,11 @@ abstract class MLFramework {
}
Future<bool> _canDownload() async {
final connectivityResult = await (Connectivity().checkConnectivity());
return connectivityResult != ConnectivityResult.mobile ||
shouldDownloadOverMobileData;
final List<ConnectivityResult> connections =
await (Connectivity().checkConnectivity());
final bool isConnectedToMobile =
connections.contains(ConnectivityResult.mobile);
return !isConnectedToMobile || shouldDownloadOverMobileData;
}
}

View file

@ -45,7 +45,9 @@ class SyncService {
sync();
});
Connectivity().onConnectivityChanged.listen((ConnectivityResult result) {
Connectivity()
.onConnectivityChanged
.listen((List<ConnectivityResult> result) {
_logger.info("Connectivity change detected " + result.toString());
if (Configuration.instance.hasConfiguredAccount()) {
sync();

View file

@ -355,9 +355,10 @@ class FileUploader {
if (isForceUpload) {
return;
}
final connectivityResult = await (Connectivity().checkConnectivity());
final List<ConnectivityResult> connections =
await (Connectivity().checkConnectivity());
bool canUploadUnderCurrentNetworkConditions = true;
if (connectivityResult == ConnectivityResult.mobile) {
if (connections.any((element) => element == ConnectivityResult.mobile)) {
canUploadUnderCurrentNetworkConditions =
Configuration.instance.shouldBackupOverMobileData();
}

View file

@ -113,38 +113,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.6"
bonsoir:
dependency: transitive
description:
name: bonsoir
sha256: "800d77c0581fff06cc43ef2b7723dfe5ee9b899ab0fdf80fb1c7b8829a5deb5c"
url: "https://pub.dev"
source: hosted
version: "3.0.0+1"
bonsoir_android:
dependency: transitive
description:
name: bonsoir_android
sha256: "7207c36fd7e0f3c7c2d8cf353f02bd640d96e2387d575837f8ac051c9cbf4aa7"
url: "https://pub.dev"
source: hosted
version: "3.0.0+1"
bonsoir_darwin:
dependency: transitive
description:
name: bonsoir_darwin
sha256: "7211042c85da2d6efa80c0976bbd9568f2b63624097779847548ed4530675ade"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
bonsoir_platform_interface:
dependency: transitive
description:
name: bonsoir_platform_interface
sha256: "64d57cd52bd477b4891e9b9d419e6408da171ed9e0efc8aa716e7e343d5d93ad"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
boolean_selector:
dependency: transitive
description:
@ -241,14 +209,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.1"
cast:
dependency: "direct main"
description:
name: cast
sha256: b70f6be547a53481dffec93ad3cc4974fae5ed707f0b677d4a50c329d7299b98
url: "https://pub.dev"
source: hosted
version: "2.0.0"
characters:
dependency: transitive
description:
@ -326,18 +286,18 @@ packages:
dependency: "direct main"
description:
name: connectivity_plus
sha256: "77a180d6938f78ca7d2382d2240eb626c0f6a735d0bfdce227d8ffb80f95c48b"
sha256: ebe15d94de9dd7c31dc2ac54e42780acdf3384b1497c69290c9f3c5b0279fc57
url: "https://pub.dev"
source: hosted
version: "4.0.2"
version: "6.0.2"
connectivity_plus_platform_interface:
dependency: transitive
description:
name: connectivity_plus_platform_interface
sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a
sha256: b6a56efe1e6675be240de39107281d4034b64ac23438026355b4234042a35adb
url: "https://pub.dev"
source: hosted
version: "1.2.4"
version: "2.0.0"
convert:
dependency: transitive
description:
@ -1769,14 +1729,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.0"
protobuf:
dependency: transitive
description:
name: protobuf
sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
provider:
dependency: "direct main"
description:

View file

@ -27,7 +27,6 @@ dependencies:
battery_info: ^1.1.1
bip39: ^1.0.6
cached_network_image: ^3.0.0
cast: ^2.0.0
chewie:
git:
url: https://github.com/ente-io/chewie.git
@ -37,11 +36,7 @@ dependencies:
collection: # dart
computer:
git: "https://github.com/ente-io/computer.git"
connectivity_plus:
git:
url: https://github.com/ente-io/plus_plugins.git
ref: check_mobile_first
path: packages/connectivity_plus/connectivity_plus/
connectivity_plus: ^6.0.2
cross_file: ^0.3.3
crypto: ^3.0.2
cupertino_icons: ^1.0.0
@ -175,7 +170,6 @@ dependencies:
xml: ^6.3.0
dependency_overrides:
connectivity_plus: ^4.0.0
# Remove this after removing dependency from flutter_sodium.
# Newer flutter packages depends on ffi > 2.0.0 while flutter_sodium depends on ffi < 2.0.0
ffi: 2.1.0

View file

@ -56,8 +56,11 @@ export default function SearchInput(props: Iprops) {
const [value, setValue] = useState<SearchOption>(null);
const appContext = useContext(AppContext);
const handleChange = (value: SearchOption) => {
setValue(value);
setQuery(value.label);
if (value) {
setValue(value);
setQuery(value.label);
}
blur();
};
const handleInputChange = (value: string, actionMeta: InputActionMeta) => {

View file

@ -7,7 +7,6 @@ import { CustomError } from "@ente/shared/error";
import { Events, eventBus } from "@ente/shared/events";
import { Remote } from "comlink";
import { FILE_TYPE } from "constants/file";
import isElectron from "is-electron";
import { EnteFile } from "types/file";
import {
generateStreamFromArrayBuffer,
@ -89,11 +88,12 @@ class DownloadManagerImpl {
e,
);
}
try {
if (isElectron()) this.fileCache = await openCache("files");
} catch (e) {
log.error("Failed to open file cache, will continue without it", e);
}
// TODO (MR): Revisit full file caching cf disk space usage
// try {
// if (isElectron()) this.fileCache = await openCache("files");
// } catch (e) {
// log.error("Failed to open file cache, will continue without it", e);
// }
this.cryptoWorker = await ComlinkCryptoWorker.getInstance();
this.ready = true;
eventBus.on(Events.LOGOUT, this.logoutHandler.bind(this), this);

View file

@ -34,6 +34,7 @@ import {
mergeMetadata,
} from "utils/file";
import { safeDirectoryName, safeFileName } from "utils/native-fs";
import { writeStream } from "utils/native-stream";
import { getAllLocalCollections } from "../collectionService";
import downloadManager from "../download";
import { getAllLocalFiles } from "../fileService";
@ -884,7 +885,7 @@ class ExportService {
try {
const exportRecord = await this.getExportRecord(folder);
const newRecord: ExportRecord = { ...exportRecord, ...newData };
await ensureElectron().saveFileToDisk(
await ensureElectron().fs.writeFile(
`${folder}/${exportRecordFileName}`,
JSON.stringify(newRecord, null, 2),
);
@ -907,8 +908,7 @@ class ExportService {
if (!(await fs.exists(exportRecordJSONPath))) {
return this.createEmptyExportRecord(exportRecordJSONPath);
}
const recordFile =
await electron.readTextFile(exportRecordJSONPath);
const recordFile = await fs.readTextFile(exportRecordJSONPath);
try {
return JSON.parse(recordFile);
} catch (e) {
@ -993,7 +993,7 @@ class ExportService {
fileExportName,
file,
);
await electron.saveStreamToDisk(
await writeStream(
`${collectionExportPath}/${fileExportName}`,
updatedFileStream,
);
@ -1044,7 +1044,7 @@ class ExportService {
imageExportName,
file,
);
await electron.saveStreamToDisk(
await writeStream(
`${collectionExportPath}/${imageExportName}`,
imageStream,
);
@ -1056,7 +1056,7 @@ class ExportService {
file,
);
try {
await electron.saveStreamToDisk(
await writeStream(
`${collectionExportPath}/${videoExportName}`,
videoStream,
);
@ -1077,7 +1077,7 @@ class ExportService {
fileExportName: string,
file: EnteFile,
) {
await ensureElectron().saveFileToDisk(
await ensureElectron().fs.writeFile(
getFileMetadataExportPath(collectionExportPath, fileExportName),
getGoogleLikeMetadataFile(fileExportName, file),
);
@ -1106,7 +1106,7 @@ class ExportService {
private createEmptyExportRecord = async (exportRecordJSONPath: string) => {
const exportRecord: ExportRecord = NULL_EXPORT_RECORD;
await ensureElectron().saveFileToDisk(
await ensureElectron().fs.writeFile(
exportRecordJSONPath,
JSON.stringify(exportRecord, null, 2),
);

View file

@ -53,6 +53,7 @@ import { VISIBILITY_STATE } from "types/magicMetadata";
import { FileTypeInfo } from "types/upload";
import { isArchivedFile, updateMagicMetadata } from "utils/magicMetadata";
import { safeFileName } from "utils/native-fs";
import { writeStream } from "utils/native-stream";
const WAIT_TIME_IMAGE_CONVERSION = 30 * 1000;
@ -798,55 +799,47 @@ async function downloadFileDesktop(
electron: Electron,
fileReader: FileReader,
file: EnteFile,
downloadPath: string,
downloadDir: string,
) {
const fileStream = (await DownloadManager.getFile(
const fs = electron.fs;
const stream = (await DownloadManager.getFile(
file,
)) as ReadableStream<Uint8Array>;
const updatedFileStream = await getUpdatedEXIFFileForDownload(
const updatedStream = await getUpdatedEXIFFileForDownload(
fileReader,
file,
fileStream,
stream,
);
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
const fileBlob = await new Response(updatedFileStream).blob();
const fileBlob = await new Response(updatedStream).blob();
const livePhoto = await decodeLivePhoto(file, fileBlob);
const imageExportName = await safeFileName(
downloadPath,
downloadDir,
livePhoto.imageNameTitle,
electron.fs.exists,
fs.exists,
);
const imageStream = generateStreamFromArrayBuffer(livePhoto.image);
await electron.saveStreamToDisk(
`${downloadPath}/${imageExportName}`,
imageStream,
);
await writeStream(`${downloadDir}/${imageExportName}`, imageStream);
try {
const videoExportName = await safeFileName(
downloadPath,
downloadDir,
livePhoto.videoNameTitle,
electron.fs.exists,
fs.exists,
);
const videoStream = generateStreamFromArrayBuffer(livePhoto.video);
await electron.saveStreamToDisk(
`${downloadPath}/${videoExportName}`,
videoStream,
);
await writeStream(`${downloadDir}/${videoExportName}`, videoStream);
} catch (e) {
await electron.fs.rm(`${downloadPath}/${imageExportName}`);
await fs.rm(`${downloadDir}/${imageExportName}`);
throw e;
}
} else {
const fileExportName = await safeFileName(
downloadPath,
downloadDir,
file.metadata.title,
electron.fs.exists,
);
await electron.saveStreamToDisk(
`${downloadPath}/${fileExportName}`,
updatedFileStream,
fs.exists,
);
await writeStream(`${downloadDir}/${fileExportName}`, updatedStream);
}
}

View file

@ -0,0 +1,39 @@
/**
* @file Streaming IPC communication with the Node.js layer of our desktop app.
*
* NOTE: These functions only work when we're running in our desktop app.
*/
/**
* Write the given stream to a file on the local machine.
*
* **This only works when we're running in our desktop app**. It uses the
* "stream://" protocol handler exposed by our custom code in the Node.js layer.
* See: [Note: IPC streams].
*
* @param path The path on the local machine where to write the file to.
* @param stream The stream which should be written into the file.
* */
export const writeStream = async (path: string, stream: ReadableStream) => {
// The duplex parameter needs to be set to 'half' when streaming requests.
//
// Currently browsers, and specifically in our case, since this code runs
// only within our desktop (Electron) app, Chromium, don't support 'full'
// duplex mode (i.e. streaming both the request and the response).
// https://developer.chrome.com/docs/capabilities/web-apis/fetch-streaming-requests
//
// In another twist, the TypeScript libdom.d.ts does not include the
// "duplex" parameter, so we need to cast to get TypeScript to let this code
// through. e.g. see https://github.com/node-fetch/node-fetch/issues/1769
const req = new Request(`stream://write${path}`, {
// GET can't have a body
method: "POST",
body: stream,
duplex: "half",
} as unknown as RequestInit);
const res = await fetch(req);
if (!res.ok)
throw new Error(
`Failed to write stream to ${path}: HTTP ${res.status}`,
);
};

View file

@ -59,11 +59,21 @@ const nextConfig = {
GIT_SHA: gitSHA(),
},
// https://dev.to/marcinwosinek/how-to-add-resolve-fallback-to-webpack-5-in-nextjs-10-i6j
// Customize the webpack configuration used by Next.js
webpack: (config, { isServer }) => {
// https://dev.to/marcinwosinek/how-to-add-resolve-fallback-to-webpack-5-in-nextjs-10-i6j
if (!isServer) {
config.resolve.fallback.fs = false;
}
// Suppress the warning "Critical dependency: require function is used
// in a way in which dependencies cannot be statically extracted" when
// import heic-convert.
//
// Upstream issue, which currently doesn't have a workaround.
// https://github.com/catdad-experiments/libheif-js/issues/23
config.ignoreWarnings = [{ module: /libheif-js/ }];
return config;
},
};

View file

@ -188,6 +188,17 @@ export interface Electron {
* Delete the file at {@link path}.
*/
rm: (path: string) => Promise<void>;
/** Read the string contents of a file at {@link path}. */
readTextFile: (path: string) => Promise<string>;
/**
* Write a string to a file, replacing the file if it already exists.
*
* @param path The path of the file.
* @param contents The string contents to write.
*/
writeFile: (path: string, contents: string) => Promise<void>;
};
/*
@ -300,12 +311,6 @@ export interface Electron {
) => Promise<void>;
// - FS legacy
saveStreamToDisk: (
path: string,
fileStream: ReadableStream,
) => Promise<void>;
saveFileToDisk: (path: string, contents: string) => Promise<void>;
readTextFile: (path: string) => Promise<string>;
isFolder: (dirPath: string) => Promise<boolean>;
// - Upload

View file

@ -1,7 +1,11 @@
export async function sleep(time: number) {
await new Promise((resolve) => {
setTimeout(() => resolve(null), time);
});
/**
* Wait for {@link ms} milliseconds
*
* This function is a promisified `setTimeout`. It returns a promise that
* resolves after {@link ms} milliseconds.
*/
export async function sleep(ms: number) {
await new Promise((resolve) => setTimeout(resolve, ms));
}
export function downloadAsFile(filename: string, content: string) {