take the db changes out, this branch now builds on chore/rust-native-core

This commit is contained in:
Santo Shakil
2026-09-28 19:05:55 +06:00
parent 5c952a1f2d
commit 3e109ffd3c
28 changed files with 13 additions and 970 deletions
-3
View File
@@ -81,9 +81,6 @@ jobs:
- name: Perform codegen
run: mise //mobile:codegen
- name: Verify generated files have not changed
run: git diff --exit-code -- ../native/crates/immich_db/src/schema.g.rs
- name: Run analyze
run: mise //mobile:analyze --full
@@ -15,7 +15,6 @@ import androidx.work.ListenableWorker
import androidx.work.WorkerParameters
import app.alextran.immich.MainActivity
import app.alextran.immich.R
import app.alextran.immich.core.NativeCore
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture
@@ -77,7 +76,7 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
}
try {
NativeCore.log(ctx, NativeCore.Level.INFO, TAG, "Background worker started")
Log.i(TAG, "Starting background upload worker")
if (!loader.initialized()) {
loader.startInitialization(ctx)
@@ -194,7 +193,7 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
* This is also called when the worker has been explicitly cancelled or replaced
*/
override fun onStopped() {
NativeCore.log(ctx, NativeCore.Level.INFO, TAG, "Background worker stopped")
Log.d(TAG, "About to stop BackupWorker")
close()
}
@@ -226,15 +225,7 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
* - Parameter success: Indicates whether the background task completed successfully
*/
private fun complete(success: Result) {
if (isComplete) {
return
}
if (success is Result.Success) {
NativeCore.log(ctx, NativeCore.Level.INFO, TAG, "Background worker finished: success")
} else {
NativeCore.log(ctx, NativeCore.Level.WARNING, TAG, "Background worker finished: failure")
}
Log.d(TAG, "About to complete BackupWorker with result: $success")
isComplete = true
if (engine != null) {
MainActivity.cancelPlugins(engine!!)
@@ -1,21 +0,0 @@
package app.alextran.immich.core
import android.content.Context
import android.util.Log
import java.util.concurrent.Executors
object NativeCore {
// Matches ImmichCoreLogLevel.
enum class Level(val value: Int, val priority: Int) { INFO(0, Log.INFO), WARNING(1, Log.WARN), SEVERE(2, Log.ERROR) }
private val loaded by lazy { runCatching { System.loadLibrary("immich_core_ffi") }.isSuccess }
private val queue = Executors.newSingleThreadExecutor()
fun log(ctx: Context, level: Level, logger: String, message: String) {
Log.println(level.priority, logger, message)
val dir = ctx.getDir("flutter", Context.MODE_PRIVATE).absolutePath
queue.execute { if (loaded) runCatching { nativeLog(dir, level.value, logger, message) } }
}
private external fun nativeLog(dir: String, level: Int, logger: String, message: String)
}
@@ -80,8 +80,6 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
* starts the engine, and sets up a timeout timer if specified.
*/
func run() {
NativeCore.log(level: ImmichCoreLogLevel_Info, logger: "BackgroundWorker", message: "Background worker started")
// Start the Flutter engine with the specified callback as the entry point
let isRunning = engine.run(
withEntrypoint: "backgroundSyncNativeEntrypoint",
@@ -90,7 +88,6 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
// Verify that the Flutter engine started successfully
if !isRunning {
NativeCore.log(level: ImmichCoreLogLevel_Severe, logger: "BackgroundWorker", message: "Background engine failed to start")
complete(success: false)
return
}
@@ -106,9 +103,6 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
if maxSeconds != nil {
// Schedule a timer to cancel the task after the specified timeout period
Timer.scheduledTimer(withTimeInterval: TimeInterval(maxSeconds!), repeats: false) { _ in
if !self.isComplete {
NativeCore.log(level: ImmichCoreLogLevel_Warning, logger: "BackgroundWorker", message: "Background worker timed out")
}
self.close()
}
}
@@ -167,12 +161,6 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
return
}
if success {
NativeCore.log(level: ImmichCoreLogLevel_Info, logger: "BackgroundWorker", message: "Background worker finished: success")
} else {
NativeCore.log(level: ImmichCoreLogLevel_Warning, logger: "BackgroundWorker", message: "Background worker finished: failure")
}
isComplete = true
AppDelegate.cancelPlugins(with: engine)
engine.destroyContext()
@@ -114,7 +114,6 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
}
task.expirationHandler = {
NativeCore.log(level: ImmichCoreLogLevel_Warning, logger: "BackgroundWorker", message: "Background task expired by the system")
DispatchQueue.main.async {
backgroundWorker.close()
}
-28
View File
@@ -1,28 +0,0 @@
import Foundation
enum NativeCore {
private static let library = dlopen("@rpath/immich_core_ffi.framework/immich_core_ffi", RTLD_NOW)
private static let queue = DispatchQueue(label: "app.alextran.immich.core")
private static func symbol<T>(_ name: String, as type: T.Type) -> T? {
guard let library, let symbol = dlsym(library, name) else { return nil }
return unsafeBitCast(symbol, to: type)
}
private static let coreLog = symbol("immich_core_log", as: ImmichCoreLogFn.self)
static func log(level: ImmichCoreLevel, logger: String, message: String) {
NSLog("%@: %@", logger, message)
let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].path
queue.async {
guard let coreLog else { return }
dir.withCString { dir in
logger.withCString { logger in
message.withCString { message in
_ = coreLog(dir, numericCast(level.rawValue), logger, message)
}
}
}
}
}
}
@@ -1,7 +1 @@
#import "GeneratedPluginRegistrant.h"
#include "../../../native/crates/immich_core_ffi/include/immich_core.h"
// cbindgen gives the enum tag and its int typedef the same name, so Swift needs a name for the tag.
// The function pointer type comes from the declaration, so nothing is retyped here.
typedef enum ImmichCoreLogLevel ImmichCoreLevel;
typedef __typeof__(&immich_core_log) ImmichCoreLogFn;
-14
View File
@@ -37,7 +37,6 @@ depends = [
"codegen:dart",
"codegen:drift:migration",
"codegen:drift:schema",
"codegen:native:schema",
"codegen:pigeon",
"codegen:translation",
]
@@ -92,19 +91,6 @@ run = [
"dart run ffigen --config ffigen.yaml",
]
[tasks."codegen:native:schema"]
description = "Generate the sqlite schema for the native core"
sources = [
"tool/generate_native_schema.dart",
"lib/data/db/**/*.dart",
"lib/domain/models/log.model.dart",
"lib/domain/models/settings_key.dart",
]
outputs = ["../native/crates/immich_db/src/schema.g.rs"]
run = "flutter test tool/generate_native_schema.dart"
depends = ["//:open-api-dart"]
wait_for = ["codegen:dart", "codegen:drift:migration", "codegen:pigeon", "codegen:translation"]
[tasks."codegen:pigeon"]
alias = "pigeon"
description = "Generate pigeon platform code"
+5 -6
View File
@@ -1670,12 +1670,11 @@ packages:
sqlite3:
dependency: "direct main"
description:
path: sqlite3
ref: "507aaaf1150059176a77e7e9562d3ff4eb901a11"
resolved-ref: "507aaaf1150059176a77e7e9562d3ff4eb901a11"
url: "https://github.com/simolus3/sqlite3.dart"
source: git
version: "3.5.2"
name: sqlite3
sha256: "61c7930bebf32c552ac5808c91bf33802e66711c9216090d3b5579c9f862e80c"
url: "https://pub.dev"
source: hosted
version: "3.4.0"
sqlite3_connection_pool:
dependency: "direct main"
description:
-17
View File
@@ -123,23 +123,6 @@ dependency_overrides:
url: https://github.com/mertalev/native
ref: '2915556701f4734a784222f290a82adb1b101682' # https://github.com/dart-lang/native/pull/3458
path: pkgs/objective_c/
sqlite3:
git:
url: https://github.com/simolus3/sqlite3.dart
ref: '507aaaf1150059176a77e7e9562d3ff4eb901a11' # https://github.com/simolus3/sqlite3.dart/pull/406, until the next sqlite3 release
path: sqlite3/
# drift and sqlite_async use the sqlite compiled into the native core instead of their own copy.
hooks:
user_defines:
sqlite3:
source:
android: system
ios: system
default: sqlite3 # host (flutter test on macos/linux) keeps the prebuilt, as today
name:
android: immich_core_ffi
ios: immich_core_ffi.framework/immich_core_ffi
flutter:
uses-material-design: true
-73
View File
@@ -1,73 +0,0 @@
// Writes the sqlite schema the native core uses to native/crates/immich_db. Drift needs dart:ui, so
// this runs under `flutter test` (see codegen:native:schema in mise.toml) rather than `dart run`.
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/data/db/logger/database.dart';
import 'package:immich_mobile/data/db/main/database.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/domain/models/settings_key.dart';
const _output = '../native/crates/immich_db/src/schema.g.rs';
void main() {
test('generate native schema', () async {
final main = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
final logs = DriftLogger.fromExecutor(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
final sections = [
'// generated by mobile/tool/generate_native_schema.dart, do not edit',
'pub const MAIN_SCHEMA_VERSION: i32 = ${await _version(main)};\npub const LOGS_SCHEMA_VERSION: i32 = ${await _version(logs)};',
await _table(main, main.settingsEntity, [main.settingsEntity.key, main.settingsEntity.value]),
await _table(logs, logs.logMessageEntity, [
logs.logMessageEntity.message,
logs.logMessageEntity.details,
logs.logMessageEntity.level,
logs.logMessageEntity.createdAt,
logs.logMessageEntity.logger,
logs.logMessageEntity.stack,
]),
'pub const LOG_LEVEL_SETTING: &str = "${SettingsKey.logLevel.name}";',
[
'#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]',
'#[repr(i64)]',
'pub enum LogLevel {',
for (final level in LogLevel.values) ' ${_pascal(level.name)} = ${level.index},',
'}',
'',
'impl LogLevel {',
' pub fn from_name(name: &str) -> Option<Self> {',
' match name {',
for (final level in LogLevel.values) ' "${level.name}" => Some(Self::${_pascal(level.name)}),',
' _ => None,',
' }',
' }',
'}',
].join('\n'),
];
await main.close();
await logs.close();
await File(_output).writeAsString('${sections.join('\n\n')}\n');
});
}
Future<int> _version(GeneratedDatabase db) async =>
(await db.customSelect('PRAGMA user_version').getSingle()).read<int>('user_version');
Future<String> _table(GeneratedDatabase db, TableInfo table, List<GeneratedColumn> columns) async {
final name = table.actualTableName;
final ddl = await db
.customSelect('SELECT sql FROM sqlite_master WHERE name = ?', variables: [Variable.withString(name)])
.getSingle();
return [
'pub mod $name {',
' pub const TABLE: &str = "$name";',
' pub const SQL: &str = r#"${ddl.read<String>('sql')}"#;',
for (final column in columns) ' pub const ${column.name.toUpperCase()}: &str = "${column.name}";',
' pub const COLUMNS: &[&str] = &[${columns.map((column) => column.name.toUpperCase()).join(', ')}];',
'}',
].join('\n');
}
String _pascal(String name) => '${name[0].toUpperCase()}${name.substring(1)}';
-35
View File
@@ -1,35 +0,0 @@
# The compile options of the sqlite package:sqlite3 ships (its doc/hook.md), so drift sees no
# difference now that it runs on this copy. The -U lines drop what libsqlite3-sys adds on top.
[env]
LIBSQLITE3_FLAGS = """
-USQLITE_DEFAULT_FOREIGN_KEYS
-USQLITE_ENABLE_API_ARMOR
-USQLITE_ENABLE_COLUMN_METADATA
-USQLITE_ENABLE_FTS3
-USQLITE_ENABLE_FTS3_PARENTHESIS
-USQLITE_ENABLE_LOAD_EXTENSION
-USQLITE_ENABLE_MEMORY_MANAGEMENT
-USQLITE_ENABLE_STAT4
-USQLITE_SOUNDEX
-USQLITE_TEMP_STORE
-USQLITE_USE_URI
SQLITE_DEFAULT_MEMSTATUS=0
SQLITE_DQS=0
SQLITE_ENABLE_BATCH_ATOMIC_WRITE
SQLITE_ENABLE_MATH_FUNCTIONS
SQLITE_ENABLE_PREUPDATE_HOOK
SQLITE_ENABLE_SESSION
SQLITE_HAVE_ISNAN
SQLITE_MAX_EXPR_DEPTH=0
SQLITE_OMIT_AUTHORIZATION
SQLITE_OMIT_DECLTYPE
SQLITE_OMIT_DEPRECATED
SQLITE_OMIT_PROGRESS_CALLBACK
SQLITE_OMIT_SHARED_CACHE
SQLITE_OMIT_TCL_VARIABLE
SQLITE_OMIT_TRACE
SQLITE_STRICT_SUBTYPE=1
SQLITE_TEMP_STORE=2
SQLITE_UNTESTABLE
SQLITE_USE_ALLOCA
"""
-85
View File
@@ -32,16 +32,6 @@ dependencies = [
"toml",
]
[[package]]
name = "cc"
version = "1.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
@@ -74,30 +64,12 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "fallible-iterator"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fastrand"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
[[package]]
name = "find-msvc-tools"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
[[package]]
name = "getrandom"
version = "0.4.3"
@@ -131,17 +103,8 @@ version = "0.1.0"
dependencies = [
"cbindgen",
"immich_core",
"immich_db",
"jni",
"libc",
"rusqlite",
]
[[package]]
name = "immich_db"
version = "0.1.0"
dependencies = [
"rusqlite",
]
[[package]]
@@ -215,17 +178,6 @@ version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "libsqlite3-sys"
version = "0.38.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1d20bef17f513b9b3004532233187769cd072d790971f4e4da0e346eb6401e8"
dependencies = [
"cc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
@@ -250,12 +202,6 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "pkg-config"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
[[package]]
name = "proc-macro2"
version = "1.0.107"
@@ -280,19 +226,6 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rusqlite"
version = "0.40.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23f2a97da3e3873c73cb2a2e71b35c40ff95e0b1eefa8d72d8499a6928c3b5b3"
dependencies = [
"bitflags",
"fallible-iterator",
"fallible-streaming-iterator",
"libsqlite3-sys",
"smallvec",
]
[[package]]
name = "rustc_version"
version = "0.4.1"
@@ -382,12 +315,6 @@ dependencies = [
"serde_core",
]
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "simd_cesu8"
version = "1.2.0"
@@ -404,12 +331,6 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "smallvec"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f"
[[package]]
name = "syn"
version = "2.0.119"
@@ -510,12 +431,6 @@ version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "walkdir"
version = "2.5.0"
+1 -2
View File
@@ -1,6 +1,6 @@
[workspace]
resolver = "3"
members = ["crates/immich_core", "crates/immich_core_ffi", "crates/immich_db"]
members = ["crates/immich_core", "crates/immich_core_ffi"]
[workspace.package]
version = "0.1.0"
@@ -11,7 +11,6 @@ license = "AGPL-3.0-only"
cbindgen = { version = "0.29", default-features = false }
jni = { version = "0.22", default-features = false }
libc = { version = "0.2", default-features = false }
rusqlite = { version = "0.40", default-features = false }
[workspace.lints.clippy]
undocumented_unsafe_blocks = "deny"
-1
View File
@@ -6,7 +6,6 @@ in `mobile/` sets one up, otherwise install [rustup](https://rustup.rs) once and
fetches the pinned toolchain itself.
- `crates/immich_core`: the shared logic
- `crates/immich_db`: the app's sqlite databases, schema generated from the Flutter side
- `crates/immich_core_ffi`: the C ABI, cbindgen writes `include/immich_core.h`
- `immich_native_core`: Flutter package with the build hook and the ffigen bindings
(`NOTICES` carries the licenses of the crates compiled into the core, update it with the dependencies)
-4
View File
@@ -11,7 +11,6 @@ crate-type = ["cdylib", "staticlib"]
[dependencies]
immich_core = { path = "../immich_core" }
immich_db = { path = "../immich_db" }
libc = { workspace = true }
[target.'cfg(target_os = "android")'.dependencies]
@@ -20,8 +19,5 @@ jni = { workspace = true }
[build-dependencies]
cbindgen = { workspace = true }
[dev-dependencies]
rusqlite = { workspace = true }
[lints]
workspace = true
+1 -19
View File
@@ -1,29 +1,11 @@
use std::env;
use std::fs;
use std::path::Path;
fn main() {
// Files that define exported items. A directory here would make Flutter rerun the hook every build.
println!("cargo:rerun-if-changed=src/lib.rs");
println!("cargo:rerun-if-changed=src/log.rs");
println!("cargo:rerun-if-changed=cbindgen.toml");
let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
cbindgen::generate(&crate_dir)
.unwrap_or_else(|e| panic!("cbindgen failed: {e}"))
.write_to_file(Path::new(&crate_dir).join("include/immich_core.h"));
// rustc exports only its own no_mangle items from the cdylib. package:sqlite3 loads this
// library for the sqlite compiled into it, so its C API has to be visible too.
match env::var("CARGO_CFG_TARGET_OS").unwrap().as_str() {
"ios" | "macos" => println!("cargo:rustc-cdylib-link-arg=-Wl,-exported_symbol,_sqlite3*"),
"android" => {
let map = Path::new(&env::var("OUT_DIR").unwrap()).join("sqlite3.map");
fs::write(&map, "{ global: sqlite3*; };\n").unwrap();
println!(
"cargo:rustc-cdylib-link-arg=-Wl,--version-script={}",
map.display()
);
}
_ => {}
}
}
@@ -1,6 +1,3 @@
language = "C"
pragma_once = true
autogen_warning = "// Generated by cbindgen. Do not edit."
[enum]
prefix_with_name = true
+1 -28
View File
@@ -1,35 +1,8 @@
use jni::EnvUnowned;
use jni::errors::ThrowRuntimeExAndDefault;
use jni::objects::{JByteBuffer, JClass, JObject, JString};
use jni::objects::{JByteBuffer, JClass, JObject};
use jni::sys::{jint, jlong, jobject};
use super::log::ImmichCoreLogLevel;
#[unsafe(no_mangle)]
pub extern "system" fn Java_app_alextran_immich_core_NativeCore_nativeLog<'caller>(
mut env: EnvUnowned<'caller>,
_this: JObject<'caller>,
dir: JString<'caller>,
level: jint,
logger: JString<'caller>,
message: JString<'caller>,
) {
let level = match level {
0 => ImmichCoreLogLevel::Info,
1 => ImmichCoreLogLevel::Warning,
2 => ImmichCoreLogLevel::Severe,
_ => return,
};
env.with_env(|env| -> jni::errors::Result<()> {
let dir = dir.mutf8_chars(env)?.to_str().into_owned();
let logger = logger.mutf8_chars(env)?.to_str().into_owned();
let message = message.mutf8_chars(env)?.to_str().into_owned();
super::log::log(&dir, level, &logger, &message);
Ok(())
})
.resolve::<ThrowRuntimeExAndDefault>();
}
#[unsafe(no_mangle)]
pub extern "system" fn Java_app_alextran_immich_NativeBuffer_allocate<'caller>(
_env: EnvUnowned<'caller>,
-3
View File
@@ -4,9 +4,6 @@ use std::ptr;
#[cfg(target_os = "android")]
mod android;
mod log;
pub use log::ImmichCoreLogLevel;
/// Returns the core version as a C string. Free it with `immich_core_free_string`.
#[unsafe(no_mangle)]
-238
View File
@@ -1,238 +0,0 @@
use std::ffi::{CStr, c_char};
use std::path::Path;
use immich_db::logs::{self, Record};
use immich_db::schema::{self, LOGS_SCHEMA_VERSION, LogLevel, MAIN_SCHEMA_VERSION};
use immich_db::{Db, settings};
use super::guard;
#[repr(i32)]
pub enum ImmichCoreLogLevel {
Info = 0,
Warning = 1,
Severe = 2,
}
/// Writes one app log entry unless it is below the app's log level setting.
/// Returns zero on success, nonzero on failure.
///
/// # Safety
/// String pointers must be null or valid NUL-terminated strings.
/// `level` must be a valid `ImmichCoreLogLevel` variant.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn immich_core_log(
app_dir: *const c_char,
level: ImmichCoreLogLevel,
logger: *const c_char,
message: *const c_char,
) -> i32 {
guard(1, || {
if app_dir.is_null() || logger.is_null() || message.is_null() {
return 1;
}
// SAFETY: pointers are non-null; the caller guarantees valid NUL-terminated strings.
let (dir, logger, message) = unsafe {
(
CStr::from_ptr(app_dir),
CStr::from_ptr(logger),
CStr::from_ptr(message),
)
};
match (dir.to_str(), logger.to_str(), message.to_str()) {
(Ok(dir), Ok(logger), Ok(message)) => log(dir, level, logger, message),
_ => 1,
}
})
}
pub(super) fn log(dir: &str, level: ImmichCoreLogLevel, logger: &str, message: &str) -> i32 {
let level = match level {
ImmichCoreLogLevel::Info => LogLevel::Info,
ImmichCoreLogLevel::Warning => LogLevel::Warning,
ImmichCoreLogLevel::Severe => LogLevel::Severe,
};
let dir = Path::new(dir);
let threshold = match Db::open(
dir,
"immich",
MAIN_SCHEMA_VERSION,
&[(schema::settings::TABLE, schema::settings::COLUMNS)],
)
.and_then(|db| settings::log_level(&db))
{
Ok(Some(threshold)) => threshold,
// No row means the default level, and an unreadable setting is treated the same.
_ => LogLevel::Info,
};
if level < threshold {
return 0;
}
let result = Db::open(
dir,
"immich_logs",
LOGS_SCHEMA_VERSION,
&[(
schema::logger_messages::TABLE,
schema::logger_messages::COLUMNS,
)],
)
.and_then(|db| {
logs::write(
&db,
&Record {
level,
logger,
message,
details: None,
},
)
});
i32::from(result.is_err())
}
#[cfg(test)]
mod tests {
use super::*;
use rusqlite::Connection;
use std::error::Error;
use std::ffi::CString;
use std::path::PathBuf;
fn temp_dir(name: &str) -> Result<PathBuf, Box<dyn Error>> {
let dir =
std::env::temp_dir().join(format!("immich_core_ffi_{name}_{}", std::process::id()));
std::fs::create_dir(&dir)?;
Ok(dir)
}
fn create(
dir: &Path,
name: &str,
sql: &str,
version: i32,
) -> Result<Connection, Box<dyn Error>> {
let conn = Connection::open(dir.join(format!("{name}.sqlite")))?;
conn.execute_batch(sql)?;
conn.pragma_update(None, "user_version", version)?;
Ok(conn)
}
#[test]
fn log_writes_the_message() -> Result<(), Box<dyn Error>> {
let dir = temp_dir("write")?;
let conn = create(
&dir,
"immich_logs",
schema::logger_messages::SQL,
LOGS_SCHEMA_VERSION,
)?;
let app_dir = CString::new(dir.to_str().ok_or("invalid test directory")?)?;
// SAFETY: all pointers reference valid C strings for the duration of the calls.
unsafe {
assert_eq!(
immich_core_log(
app_dir.as_ptr(),
ImmichCoreLogLevel::Info,
c"BackgroundWorker".as_ptr(),
c"Started engine".as_ptr()
),
0
);
assert_ne!(
immich_core_log(
app_dir.as_ptr(),
ImmichCoreLogLevel::Info,
c"BackgroundWorker".as_ptr(),
c"\xff".as_ptr()
),
0
);
}
let logged: (String, String, i64, i64) = conn.query_row(
"select message, logger, level, count(*) from logger_messages",
[],
|r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?)),
)?;
assert_eq!(
logged,
(
"Started engine".into(),
"BackgroundWorker".into(),
LogLevel::Info as i64,
1
)
);
drop(conn);
std::fs::remove_dir_all(dir)?;
Ok(())
}
#[test]
fn log_skips_levels_below_the_setting() -> Result<(), Box<dyn Error>> {
let dir = temp_dir("level")?;
let main = create(&dir, "immich", schema::settings::SQL, MAIN_SCHEMA_VERSION)?;
main.execute(
"insert into settings (key, value) values ('logLevel', 'severe')",
[],
)?;
let logs = create(
&dir,
"immich_logs",
schema::logger_messages::SQL,
LOGS_SCHEMA_VERSION,
)?;
let dir_str = dir.to_str().ok_or("invalid test directory")?;
assert_eq!(
log(
dir_str,
ImmichCoreLogLevel::Warning,
"BackgroundWorker",
"skipped"
),
0
);
assert_eq!(
log(
dir_str,
ImmichCoreLogLevel::Severe,
"BackgroundWorker",
"kept"
),
0
);
let messages: Vec<String> = logs
.prepare("select message from logger_messages")?
.query_map([], |r| r.get(0))?
.collect::<Result<_, _>>()?;
assert_eq!(messages, ["kept"]);
drop((main, logs));
std::fs::remove_dir_all(dir)?;
Ok(())
}
#[test]
fn log_failure_returns_an_error_code() {
// SAFETY: each pointer is null or references a valid C string.
unsafe {
assert_ne!(
immich_core_log(
c"/nonexistent".as_ptr(),
ImmichCoreLogLevel::Info,
c"BackgroundWorker".as_ptr(),
c"Started engine".as_ptr()
),
0
);
assert_ne!(
immich_core_log(
std::ptr::null(),
ImmichCoreLogLevel::Info,
c"BackgroundWorker".as_ptr(),
c"Started engine".as_ptr()
),
0
);
}
}
}
-11
View File
@@ -1,11 +0,0 @@
[package]
name = "immich_db"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
rusqlite = { workspace = true, features = ["bundled"] }
[lints]
workspace = true
-105
View File
@@ -1,105 +0,0 @@
use std::path::Path;
use std::time::Duration;
use rusqlite::{Connection, OpenFlags};
pub mod logs;
#[path = "schema.g.rs"]
pub mod schema;
pub mod settings;
#[derive(Debug)]
pub enum DbError {
Schema { found: i32, expected: i32 },
Column { table: String, column: String },
Sqlite(rusqlite::Error),
}
impl From<rusqlite::Error> for DbError {
fn from(e: rusqlite::Error) -> Self {
Self::Sqlite(e)
}
}
pub struct Db(Connection);
impl Db {
/// Opens `<dir>/<name>.sqlite`. The file must already exist, carry at least `version` in
/// `PRAGMA user_version` and have every column in `tables`; nothing is read or written otherwise.
pub fn open(
dir: &Path,
name: &str,
version: i32,
tables: &[(&str, &[&str])],
) -> Result<Db, DbError> {
let conn = Connection::open_with_flags(
dir.join(format!("{name}.sqlite")),
OpenFlags::SQLITE_OPEN_READ_WRITE,
)?;
conn.busy_timeout(Duration::from_secs(5))?;
Db::check(conn, version, tables)
}
fn check(conn: Connection, expected: i32, tables: &[(&str, &[&str])]) -> Result<Db, DbError> {
let found = conn.query_row("PRAGMA user_version", [], |row| row.get(0))?;
if found < expected {
return Err(DbError::Schema { found, expected });
}
for (table, columns) in tables {
let mut names = Vec::new();
conn.pragma(None, "table_info", table, |row| {
names.push(row.get::<_, String>(1)?);
Ok(())
})?;
if let Some(column) = columns.iter().find(|c| !names.iter().any(|n| n == *c)) {
return Err(DbError::Column {
table: table.to_string(),
column: column.to_string(),
});
}
}
Ok(Db(conn))
}
}
#[cfg(test)]
mod tests {
use super::*;
use schema::{LOGS_SCHEMA_VERSION, logger_messages};
const LOGS_TABLES: &[(&str, &[&str])] = &[(logger_messages::TABLE, logger_messages::COLUMNS)];
fn memory(sql: &str, version: i32) -> Result<Connection, DbError> {
let conn = Connection::open_in_memory()?;
conn.execute_batch(sql)?;
conn.pragma_update(None, "user_version", version)?;
Ok(conn)
}
#[test]
fn version_is_a_floor() -> Result<(), DbError> {
let older = memory(logger_messages::SQL, LOGS_SCHEMA_VERSION - 1)?;
assert!(matches!(
Db::check(older, LOGS_SCHEMA_VERSION, LOGS_TABLES),
Err(DbError::Schema { found, expected })
if found == LOGS_SCHEMA_VERSION - 1 && expected == LOGS_SCHEMA_VERSION
));
let newer = memory(logger_messages::SQL, LOGS_SCHEMA_VERSION + 1)?;
Db::check(newer, LOGS_SCHEMA_VERSION, LOGS_TABLES)?;
Ok(())
}
#[test]
fn missing_column_is_refused() -> Result<(), DbError> {
let conn = memory(
"create table logger_messages (id integer primary key)",
LOGS_SCHEMA_VERSION,
)?;
assert!(matches!(
Db::check(conn, LOGS_SCHEMA_VERSION, LOGS_TABLES),
Err(DbError::Column { table, column })
if table == "logger_messages" && column == "message"
));
Ok(())
}
}
-121
View File
@@ -1,121 +0,0 @@
use rusqlite::params;
use crate::schema::LogLevel;
use crate::{Db, DbError};
pub struct Record<'a> {
pub level: LogLevel,
pub logger: &'a str,
pub message: &'a str,
pub details: Option<&'a str>,
}
pub fn write(db: &Db, record: &Record<'_>) -> Result<(), DbError> {
use crate::schema::logger_messages::{
CREATED_AT, DETAILS, LEVEL, LOGGER, MESSAGE, STACK, TABLE,
};
let created_at = created_at_sql();
let sql = format!(
"insert into {TABLE} ({MESSAGE}, {DETAILS}, {LEVEL}, {CREATED_AT}, {LOGGER}, {STACK}) \
values (?1, ?2, ?3, {created_at}, ?4, null)"
);
db.0.execute(
&sql,
params![
record.message,
record.details,
record.level as i64,
record.logger
],
)?;
Ok(())
}
fn created_at_sql() -> String {
let seconds = "(strftime('%s', 'now', 'localtime') - strftime('%s', 'now'))";
format!(
"strftime('%Y-%m-%dT%H:%M:%f', 'now', 'localtime') || ' ' || {}",
offset_sql(seconds)
)
}
fn offset_sql(seconds: &str) -> String {
format!(
"printf('%s%02d:%02d', case when {seconds} < 0 then '-' else '+' end, abs({seconds}) / 3600, abs({seconds}) % 3600 / 60)"
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::{LOGS_SCHEMA_VERSION, logger_messages};
use rusqlite::Connection;
#[test]
fn write_roundtrips() -> Result<(), DbError> {
let conn = Connection::open_in_memory()?;
conn.execute_batch(logger_messages::SQL)?;
conn.pragma_update(None, "user_version", LOGS_SCHEMA_VERSION)?;
let db = Db::check(conn, LOGS_SCHEMA_VERSION, &[])?;
write(
&db,
&Record {
level: LogLevel::Info,
logger: "BackgroundWorker",
message: "Starting background upload worker",
details: None,
},
)?;
let (message, level, created_at, logger, nulls): (String, i64, String, String, bool) = db
.0
.query_row(
"select message, level, created_at, logger, details is null and stack is null from logger_messages",
[],
|r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?, r.get(4)?)),
)?;
assert_eq!(
(message.as_str(), level, logger.as_str(), nulls),
(
"Starting background upload worker",
5, // drift's LogLevel.info index
"BackgroundWorker",
true
)
);
let shape = "0000-00-00T00:00:00.000 +00:00";
let matches = created_at.len() == shape.len()
&& created_at.chars().zip(shape.chars()).all(|(c, s)| match s {
'0' => c.is_ascii_digit(),
'+' => c == '+' || c == '-',
_ => c == s,
});
assert!(matches, "{created_at}");
write(
&db,
&Record {
level: LogLevel::Severe,
logger: "BackgroundWorker",
message: "Failed to start engine",
details: Some("engine unavailable"),
},
)?;
let details: String = db.0.query_row(
"select details from logger_messages where level = ?1",
[LogLevel::Severe as i64],
|r| r.get(0),
)?;
assert_eq!(details, "engine unavailable");
Ok(())
}
#[test]
fn offset_text_has_sign_and_half_hours() -> Result<(), DbError> {
let conn = Connection::open_in_memory()?;
for (seconds, text) in [(19800, "+05:30"), (0, "+00:00"), (-1800, "-00:30")] {
let sql = format!("select {}", offset_sql(&seconds.to_string()));
let got: String = conn.query_row(&sql, [], |r| r.get(0))?;
assert_eq!(got, text);
}
Ok(())
}
}
-59
View File
@@ -1,59 +0,0 @@
// generated by mobile/tool/generate_native_schema.dart, do not edit
pub const MAIN_SCHEMA_VERSION: i32 = 31;
pub const LOGS_SCHEMA_VERSION: i32 = 1;
pub mod settings {
pub const TABLE: &str = "settings";
pub const SQL: &str = r#"CREATE TABLE "settings" ("key" TEXT NOT NULL, "value" TEXT NULL, "updated_at" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), PRIMARY KEY ("key")) WITHOUT ROWID, STRICT"#;
pub const KEY: &str = "key";
pub const VALUE: &str = "value";
pub const COLUMNS: &[&str] = &[KEY, VALUE];
}
pub mod logger_messages {
pub const TABLE: &str = "logger_messages";
pub const SQL: &str = r#"CREATE TABLE "logger_messages" ("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "message" TEXT NOT NULL, "details" TEXT NULL, "level" INTEGER NOT NULL, "created_at" TEXT NOT NULL, "logger" TEXT NULL, "stack" TEXT NULL)"#;
pub const MESSAGE: &str = "message";
pub const DETAILS: &str = "details";
pub const LEVEL: &str = "level";
pub const CREATED_AT: &str = "created_at";
pub const LOGGER: &str = "logger";
pub const STACK: &str = "stack";
pub const COLUMNS: &[&str] = &[MESSAGE, DETAILS, LEVEL, CREATED_AT, LOGGER, STACK];
}
pub const LOG_LEVEL_SETTING: &str = "logLevel";
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[repr(i64)]
pub enum LogLevel {
All = 0,
Finest = 1,
Finer = 2,
Fine = 3,
Config = 4,
Info = 5,
Warning = 6,
Severe = 7,
Shout = 8,
Off = 9,
}
impl LogLevel {
pub fn from_name(name: &str) -> Option<Self> {
match name {
"all" => Some(Self::All),
"finest" => Some(Self::Finest),
"finer" => Some(Self::Finer),
"fine" => Some(Self::Fine),
"config" => Some(Self::Config),
"info" => Some(Self::Info),
"warning" => Some(Self::Warning),
"severe" => Some(Self::Severe),
"shout" => Some(Self::Shout),
"off" => Some(Self::Off),
_ => None,
}
}
}
-44
View File
@@ -1,44 +0,0 @@
use rusqlite::OptionalExtension;
use crate::schema::settings::{KEY, TABLE, VALUE};
use crate::schema::{LOG_LEVEL_SETTING, LogLevel};
use crate::{Db, DbError};
pub fn log_level(db: &Db) -> Result<Option<LogLevel>, DbError> {
let name: Option<Option<String>> =
db.0.query_row(
&format!("select {VALUE} from {TABLE} where {KEY} = ?1"),
[LOG_LEVEL_SETTING],
|row| row.get(0),
)
.optional()?;
Ok(name.flatten().and_then(|name| LogLevel::from_name(&name)))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::{MAIN_SCHEMA_VERSION, settings};
use rusqlite::Connection;
#[test]
fn log_level_reads_the_setting() -> Result<(), DbError> {
let conn = Connection::open_in_memory()?;
conn.execute_batch(settings::SQL)?;
conn.pragma_update(None, "user_version", MAIN_SCHEMA_VERSION)?;
let db = Db::check(
conn,
MAIN_SCHEMA_VERSION,
&[(settings::TABLE, settings::COLUMNS)],
)?;
assert_eq!(log_level(&db)?, None);
db.0.execute(
"insert into settings (key, value) values (?1, 'severe')",
[LOG_LEVEL_SETTING],
)?;
assert_eq!(log_level(&db)?, Some(LogLevel::Severe));
db.0.execute("update settings set value = 'loud'", [])?;
assert_eq!(log_level(&db)?, None);
Ok(())
}
}
+1 -11
View File
@@ -1,33 +1,23 @@
bitflags
bytes
cfg-if
combine
fallible-iterator
fallible-streaming-iterator
jni
jni-sys
libc
libsqlite3-sys
log
memchr
rusqlite
simd_cesu8
simdutf8
smallvec
thiserror
Copyright (c) 2014 The Rust Project Developers
Copyright (c) 2018 Carl Lerche
Copyright (c) 2014 Alex Crichton
Copyright (c) 2015 Markus Westerlind
Copyright (c) 2015 The rust-openssl-verify Developers
Copyright (c) 2016 The fallible-streaming-iterator Developers
Copyright (c) 2016 Prevoty, Inc. and jni-rs contributors
Copyright (c) 2015 The rust-jni-sys Developers
Copyright (c) The Rust Project Developers
Copyright (c) 2014 The rusqlite developers
Copyright (c) 2014 The Rust Project Developers
Copyright (c) 2015 Andrew Gallant
Copyright (c) 2018 The Servo Project Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+1 -8
View File
@@ -10,9 +10,7 @@ const _crate = '../crates/immich_core_ffi';
const _manifests = [
'../Cargo.toml',
'../Cargo.lock',
'../.cargo/config.toml',
'../crates/immich_core/Cargo.toml',
'../crates/immich_db/Cargo.toml',
'$_crate/Cargo.toml',
'$_crate/rust-toolchain.toml',
];
@@ -29,12 +27,7 @@ void main(List<String> args) async {
await RustBuilder(
assetName: 'src/bindings.g.dart',
cratePath: _crate,
extraCargoEnvironmentVariables: {
if (code.targetOS == OS.android) 'RUSTFLAGS': _androidRustFlags(code),
// rustc links iOS for 10.0 by default, where libSystem hides the ___chkstk_darwin that clang's
// stack probes in the bundled sqlite call.
if (code.targetOS == OS.iOS) 'IPHONEOS_DEPLOYMENT_TARGET': '${code.iOS.targetVersion}.0',
},
extraCargoEnvironmentVariables: {if (code.targetOS == OS.android) 'RUSTFLAGS': _androidRustFlags(code)},
).run(input: input, output: output);
});
}