mirror of
https://github.com/immich-app/immich.git
synced 2026-09-30 13:23:21 +08:00
take the db changes out, this branch now builds on chore/rust-native-core
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
+3
-12
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)}';
|
||||
@@ -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
|
||||
"""
|
||||
Generated
-85
@@ -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
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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,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>,
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user