diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index c4906cf..7f3e80f 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -34,10 +34,12 @@ permissions: env: PROJECT: ImageStore/ImageStore.csproj + MIGRATOR_PROJECT: ImageStore.Migrator/ImageStore.Migrator.csproj CONFIGURATION: Release # dotnet publish, not build: it gathers the full runtime dependency set into # one directory, which is what a PowerShell module folder has to contain. BUILD_OUTPUT: publish + MIGRATOR_OUTPUT: publish-migrator # Date-based tags follow this timezone rather than the runner's UTC clock, so # a tag reads the same date the commit was authored in. TAG_TIMEZONE: China Standard Time @@ -116,20 +118,45 @@ jobs: --output $env:BUILD_OUTPUT ` --nologo + dotnet publish $env:MIGRATOR_PROJECT ` + --configuration $env:CONFIGURATION ` + --output $env:MIGRATOR_OUTPUT ` + --nologo + + - name: Trim non-Windows native libraries + shell: pwsh + run: | + # SQLite ships its native library for every platform it supports - + # Linux, macOS, wasm, a dozen architectures each. Both of these run only + # on Windows, so those copies are pure weight: leaving them in takes the + # module from about 4 MB to 34 MB. + # + # Done here rather than with a RuntimeIdentifier so that all three + # Windows architectures stay in the package. + foreach ($output in @($env:BUILD_OUTPUT, $env:MIGRATOR_OUTPUT)) { + $runtimes = Join-Path $output 'runtimes' + if (-not (Test-Path $runtimes)) { continue } + + $dropped = Get-ChildItem $runtimes -Directory | Where-Object { $_.Name -notlike 'win*' } + foreach ($rid in $dropped) { Remove-Item $rid.FullName -Recurse -Force } + + Write-Host "$output : removed $($dropped.Count) non-Windows runtime directories, kept $((Get-ChildItem $runtimes -Directory).Name -join ', ')" + } + - name: Verify build output shell: pwsh run: | # Import-Module fails at load time if any of these is missing from the # module folder, so publishing without them would ship a broken release. - # The BCL packages the 4.8.1 build needed (System.Buffers, System.Memory, - # System.Numerics.Vectors, System.Runtime.CompilerServices.Unsafe) are - # part of the framework on .NET 10 and no longer appear here. $required = @( 'ImageStore.dll' 'ImageStore.deps.json' 'Shipwreck.Phash.dll' 'Shipwreck.Phash.Bitmaps.dll' - 'Microsoft.Data.SqlClient.dll' + 'Microsoft.Data.Sqlite.dll' + 'SQLitePCLRaw.core.dll' + 'SQLitePCLRaw.provider.e_sqlite3.dll' + 'SQLitePCLRaw.batteries_v2.dll' ) $missing = $required | Where-Object { -not (Test-Path (Join-Path $env:BUILD_OUTPUT $_)) } if ($missing) { @@ -137,21 +164,28 @@ jobs: exit 1 } - # SqlClient's native SNI library lives in a runtimes\ subdirectory. A - # module missing it loads fine and then fails on the first connection, - # so check it separately from the flat files above. - if (-not (Get-ChildItem $env:BUILD_OUTPUT -Recurse -File -Filter 'Microsoft.Data.SqlClient.SNI.dll')) { - Write-Error "Native SNI library missing from $($env:BUILD_OUTPUT)\runtimes" + # The SQLite engine itself is a native library under runtimes\win-*\native. + # Without it the module loads and then throws on the first query, so check + # it separately from the flat files above - and check that the trim step + # above did not take it with the others. + if (-not (Test-Path (Join-Path $env:BUILD_OUTPUT 'runtimes\win-x64\native\e_sqlite3.dll'))) { + Write-Error "Native SQLite library missing from $($env:BUILD_OUTPUT)\runtimes\win-x64\native" exit 1 } + # The migrator is the only thing that still talks to Sql Server. + foreach ($f in @('ImageStore.Migrator.dll', 'Microsoft.Data.SqlClient.dll', 'Microsoft.Data.Sqlite.dll')) { + if (-not (Test-Path (Join-Path $env:MIGRATOR_OUTPUT $f))) { + Write-Error "Missing from $($env:MIGRATOR_OUTPUT): $f" + exit 1 + } + } + # And the opposite check: nothing belonging to the PowerShell host may # ship. System.Management.Automation.dll is a reference assembly with no - # method bodies, and the native payload beside it is the host's own - # (including Linux and macOS libraries, in a Windows-only module). + # method bodies, and the native payload beside it is the host's own. # ExcludeAssets="runtime;native" in the csproj keeps them out; this is # the net for a regression, since an oversized package still looks fine. - # Recursive, because the native files sit under runtimes\\native. $banned = @( 'System.Management.Automation.dll' 'pwrshplugin.dll' @@ -167,8 +201,12 @@ jobs: exit 1 } - # Recursive: Microsoft.Data.SqlClient carries a native SNI library under - # runtimes\win-*\native, and that layout has to survive into the package. + # Sql Server support was removed from the module; only the migrator keeps it. + if (Get-ChildItem $env:BUILD_OUTPUT -Recurse -File -Filter 'Microsoft.Data.SqlClient.dll') { + Write-Error "SqlClient is still being shipped inside the module package" + exit 1 + } + Get-ChildItem $env:BUILD_OUTPUT -Recurse -File | Select-Object @{n='Path';e={$_.FullName.Substring((Resolve-Path $env:BUILD_OUTPUT).Path.Length + 1)}}, Length | Sort-Object Path | @@ -187,9 +225,9 @@ jobs: # The module: the whole publish output minus debug symbols. Copied # wholesale rather than picking out *.dll, because the tree matters — - # ImageStore.deps.json drives dependency resolution, and SqlClient's - # native SNI library lives under runtimes\win-*\native. Flattening it - # produces a module that loads and then fails on first connect. + # ImageStore.deps.json drives dependency resolution, and the native + # SQLite engine lives under runtimes\win-*\native. Flattening it + # produces a module that loads and then throws on the first query. $moduleStage = Join-Path $env:RUNNER_TEMP 'module' $moduleZip = "ImageStore-$tag.zip" New-Item -ItemType Directory -Path $moduleStage -Force | Out-Null @@ -197,19 +235,21 @@ jobs: Get-ChildItem $moduleStage -Recurse -Include '*.pdb' | Remove-Item -Force Compress-Archive -Path "$moduleStage\*" -DestinationPath $moduleZip -Force - # The database: shipped as its own asset. It is a one-time download - # when setting up a project and does not change between builds, so it - # has no business inflating every module download. - $dbStage = Join-Path $env:RUNNER_TEMP 'database' - $dbZip = "ImageStore-Database-$tag.zip" - New-Item -ItemType Directory -Path $dbStage -Force | Out-Null - Copy-Item 'Database\DataStore.mdf', ` - 'Database\DataStore_log.ldf', ` - 'Database\CreateDatabase.txt' -Destination $dbStage - Compress-Archive -Path "$dbStage\*" -DestinationPath $dbZip -Force + # The migration tool: separate because it is needed once per library, by + # the few users coming from Sql Server, and it carries the whole SqlClient + # dependency tree that the module itself no longer has. + $migratorStage = Join-Path $env:RUNNER_TEMP 'migrator' + $migratorZip = "ImageStore-Migrator-$tag.zip" + New-Item -ItemType Directory -Path $migratorStage -Force | Out-Null + Copy-Item "$env:MIGRATOR_OUTPUT\*" -Destination $migratorStage -Recurse -Force + Get-ChildItem $migratorStage -Recurse -Include '*.pdb' | Remove-Item -Force + Compress-Archive -Path "$migratorStage\*" -DestinationPath $migratorZip -Force + + # There is no empty-database asset any more: New-ImageStoreDatabase + # creates the file, so there is nothing to hand out in advance. Add-Content -Path $env:GITHUB_OUTPUT -Value "module_zip=$moduleZip" - Add-Content -Path $env:GITHUB_OUTPUT -Value "database_zip=$dbZip" + Add-Content -Path $env:GITHUB_OUTPUT -Value "migrator_zip=$migratorZip" - name: Upload build artifact uses: actions/upload-artifact@v7 @@ -217,7 +257,7 @@ jobs: name: ImageStore-${{ steps.version.outputs.tag }} path: | ${{ steps.package.outputs.module_zip }} - ${{ steps.package.outputs.database_zip }} + ${{ steps.package.outputs.migrator_zip }} if-no-files-found: error - name: Publish release @@ -229,12 +269,12 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ steps.version.outputs.tag }} MODULE_ZIP: ${{ steps.package.outputs.module_zip }} - DATABASE_ZIP: ${{ steps.package.outputs.database_zip }} + MIGRATOR_ZIP: ${{ steps.package.outputs.migrator_zip }} TAG_EXISTS: ${{ steps.version.outputs.tag_exists }} run: | $tag = $env:TAG $moduleZip = $env:MODULE_ZIP - $dbZip = $env:DATABASE_ZIP + $migratorZip = $env:MIGRATOR_ZIP # Single-quoted here-string: no interpolation, so backslashes and the # indented code block survive untouched. Placeholders filled in below. @@ -242,17 +282,21 @@ jobs: ### Downloads - **__MODULE_ZIP__** — the module and its dependencies. This is the one you want. - - **__DATABASE_ZIP__** — an empty database (DataStore.mdf, DataStore_log.ldf) and the - schema script (CreateDatabase.txt). Only needed once, when setting up a new project; - the contents are the same in every release. + - **__MIGRATOR_ZIP__** — only for upgrading an existing Sql Server library. Not needed + for a new one. ### Usage - Unpack the module archive into a folder and load it: + Unpack the module archive into a folder as a whole, then: + pwsh Import-Module .\ImageStore.dll + New-ImageStoreDatabase .\library.db + + There is no database server to install: a library is a single file, and + New-ImageStoreDatabase creates it and opens it in one step. - Requires Windows, .NET Framework 4.8.1 and SQL Server 2017 (LocalDB or Express is enough). + Requires Windows, PowerShell 7.6 or later, and the .NET 10 Desktop Runtime. Built from commit __SHA__. '@ @@ -260,7 +304,7 @@ jobs: # .Replace(), not -replace: a plain string swap with no regex meaning # attached to the replacement text. $notes = $notes.Replace('__MODULE_ZIP__', $moduleZip) - $notes = $notes.Replace('__DATABASE_ZIP__', $dbZip) + $notes = $notes.Replace('__MIGRATOR_ZIP__', $migratorZip) $notes = $notes.Replace('__SHA__', $env:GITHUB_SHA) Set-Content -Path release-notes.md -Value $notes -Encoding utf8 @@ -268,7 +312,7 @@ jobs: # Splatted as an array, so each element reaches gh as one argument # without going through a shell. Both archives are attached to the # same release. - $ghArgs = @($tag, $moduleZip, $dbZip, '--title', $tag, '--notes-file', 'release-notes.md', '--generate-notes') + $ghArgs = @($tag, $moduleZip, $migratorZip, '--title', $tag, '--notes-file', 'release-notes.md', '--generate-notes') if ($env:TAG_EXISTS -ne 'true') { # Tag does not exist yet — gh creates it on the commit being built. $ghArgs += @('--target', $env:GITHUB_SHA) diff --git a/CLAUDE.md b/CLAUDE.md index eafff49..2a49497 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -48,7 +48,7 @@ Dependencies come from NuGet; nothing is committed to the repo: | Package | Note | |---|---| | `Shipwreck.Phash`, `Shipwreck.Phash.Bitmaps` | Upstream. A fork used to be vendored here for extra `GetCrossCorrelation` overloads; upstream 0.5.0 has them. | -| `Microsoft.Data.SqlClient` | Replaces `System.Data.SqlClient`, which has no .NET 10 story. | +| `Microsoft.Data.Sqlite` | The database. `ImageStore.Migrator` additionally uses `Microsoft.Data.SqlClient`, which the module itself no longer references. | | `System.Management.Automation` | `ExcludeAssets="runtime;native"` — see below. | **`ExcludeAssets` on `System.Management.Automation` must keep both `runtime` and `native`.** @@ -59,15 +59,19 @@ payload into the output — `pwrshplugin.dll`, `PowerShell.Core.Instrumentation. ~36 files and is invisible unless the package is opened. Use `dotnet publish`, not `dotnet build`, when producing something to ship: the module needs its -full dependency closure, `ImageStore.deps.json`, and `runtimes/win-*/native/Microsoft.Data.SqlClient.SNI.dll`. -A package missing SNI loads fine and then fails on the first database connection. +full dependency closure, `ImageStore.deps.json`, and `runtimes/win-*/native/e_sqlite3.dll`. +A package missing the native engine loads fine and then throws on the first query. + +SQLite ships that native library for every platform it supports. The workflow deletes every +non-`win*` runtime directory before packaging; skipping that takes the module from ~6 MB to 34 MB +of Linux and macOS binaries that a Windows-only module can never load. ## Repository layout ``` ImageStore.sln Solution; also carries every doc/*.md as SolutionItems ImageStore/ The only real project - Database/ Open/Close/Compress database cmdlets + Database/ New/Open/Close/Compress cmdlets, and SqliteSchema.cs DatabaseShared/ Connection singleton + SQL building helpers Folder/ Folder entity, helper, cmdlets IgnoredDirectory/ Directory-exclusion entity, helper, cmdlets @@ -75,7 +79,7 @@ ImageStore/ The only real project File/ File entity, hashing (Measure*), file-system operations SameFile/ SHA-1 duplicate detection + WinForms review UI SimilarFile/ pHash similarity detection + WinForms review UI + thumbprint cache -Database/ CreateDatabase.txt (full schema script) + an empty DataStore.mdf/.ldf +ImageStore.Migrator/ Console tool: copies a Sql Server library into a SQLite file doc/ User documentation: concept/, cmdlet/, type/, walkthrough/ ``` @@ -87,7 +91,11 @@ Keep that alignment when adding features. ### One class per cmdlet Every cmdlet is its own file named `Cmdlet.cs`, deriving from -`System.Management.Automation.Cmdlet` (not `PSCmdlet`). Cross-cmdlet logic lives in a +`System.Management.Automation.Cmdlet`. The two exceptions are `New-ImageStoreDatabase` and +`Open-ImageStoreDatabase`, which derive from `PSCmdlet` because they need +`GetUnresolvedProviderPathFromPSPath` — the PowerShell location and the process working directory +are routinely different, and resolving a path against the wrong one opens a file somewhere the +user did not mean. Cross-cmdlet logic lives in a `Helper.cs` static class in the same directory. Entity classes are named `ImageStore.cs` and are the public surface returned to PowerShell. @@ -96,50 +104,65 @@ Every cmdlet is its own file named `Cmdlet.cs`, deriving from Two static fields hold state for the whole PowerShell session: - `DatabaseConnection.Current` (`DatabaseShared/DatabaseConnection.cs`) — a single open - `SqlConnection`, set by `Open-ImageStoreDatabase`, torn down by `Close-ImageStoreDatabase`. - Accessing it before opening throws `InvalidOperationException("Database is not specified.")`. - Every cmdlet starts with `var connection = DatabaseConnection.Current;`. + `SqliteConnection`, set by `Open-ImageStoreDatabase` or `New-ImageStoreDatabase`, torn down by + `Close-ImageStoreDatabase`. Accessing it before opening throws + `InvalidOperationException("Database is not specified.")`. Every cmdlet starts with + `var connection = DatabaseConnection.Current;`. - `LoadImageHelper.cachePath` (`SimilarFile/LoadImageHelper.cs`) — the thumbprint cache directory, set by `Set-ImageStoreThumbprintCacheFolder` (resolved **relative to the assembly folder**), cleared by `Clear-ImageStoreThumbprintCacheFolder`. `null` means caching is disabled. Consequences to respect: there is exactly one connection, so nothing may run two DB-touching -cmdlets concurrently; and several code paths create `#temp` tables, which only work because that -one connection is reused for the whole operation. Neither setting survives a PowerShell restart. +cmdlets concurrently; and several code paths create temp tables, which are connection-scoped and +only work because that one connection is reused for the whole operation. Neither setting survives +a PowerShell restart. + +`DatabaseShared/ModuleLifetime.cs` closes the database on `Remove-Module` (`IModuleAssemblyCleanup`) +and on host exit (`AppDomain.ProcessExit`). Both are needed: neither covers the other. `Close()` is +idempotent and locked because they can race the pipeline thread. ### ADO.NET conventions -Raw `Microsoft.Data.SqlClient` throughout — no ORM, no EF, no async. Only the namespace differs -from the old `System.Data.SqlClient`; the type names are the same, and `SqlDbType` is still -`System.Data.SqlDbType`. The consistent shape is: +Raw `Microsoft.Data.Sqlite` throughout — no ORM, no EF, no async. The consistent shape is: ```csharp var connection = DatabaseConnection.Current; -using (var command = new SqlCommand("Select [Id],[Extension] from [Extension]")) +using (var command = new SqliteCommand("Select [Id],[Extension] from [Extension]")) { command.Connection = connection; command.CommandTimeout = 0; // long-running by design; do not remove - command.Parameters.Add(new SqlParameter("@Id", SqlDbType.UniqueIdentifier) { Value = id }); + command.Parameters.AddGuid("@Id", id); // never bind a Guid directly - see below using (var reader = command.ExecuteReader(CommandBehavior.SequentialAccess)) { - while (reader.Read()) { /* read by ordinal */ } + while (reader.Read()) { /* reader.GetGuid(0), GetString(1), ... */ } reader.Close(); } } ``` +- **Read with typed getters, never `(T)reader[i]`.** SQLite has five storage classes, so + `reader[i]` returns `long` for every integer and bool, `double` for every real, and `string` or + `byte[]` for a Guid. Casting directly throws `InvalidCastException`. `GetGuid`, `GetBoolean`, + `GetInt32` and `GetFloat` all convert correctly. +- **Bind Guids through `Parameters.AddGuid`** (`DatabaseShared/SqliteParameterExtensions.cs`). + Binding a `Guid` directly makes the provider store 36-character TEXT while the schema says BLOB, + and the failure is silent — queries simply match nothing. The same file has `AddBlob`, `AddText`, + `AddInt`, `AddBool` and `AddReal`. - `CommandTimeout = 0` (infinite) is deliberate — comparison passes can run for hours or days. - Columns are read **by ordinal**, so changing the `Select` list means changing the indices too. -- Nullable columns go through `DBNullableReader.ConvertFromReferenceType` / - `ConvertFromValueType`. +- Nullable columns go through `DBNullableReader.ConvertFromReferenceType`. - **Never concatenate user values into SQL.** Dynamic filters are built with `DatabaseShared/WhereCauseBuilder.cs`, which appends parameterized predicates (`AddStringComparingCause`, `AddIntComparingCause`, `AddBitComparingCause`, `AddUniqueIdentifierComparingCause`, `AddRealComparingCause`, `AddIntInRangeCause`) and emits - the final clause via `ToFullWhereCommand()`. `LIKE` values are escaped by - `SqlServerLikeValueBuilder`. + the final clause via `ToFullWhereCommand()`. +- `LIKE` values are escaped by `SqliteLikeValueBuilder`, and every `LIKE` needs + `SqliteLikeValueBuilder.EscapeClause` appended — SQLite has no character classes, so escaping + only works through an explicit `ESCAPE`. Do not reintroduce quote-doubling: these are parameters, + never parsed as SQL, and doubling corrupted searches for names containing an apostrophe. - The name "Cause" is a long-standing misspelling of "Clause" in this codebase. Match the existing spelling rather than renaming. +- SQLite has no `TOP`; row limits are `LIMIT n` **appended after** the where and order by clauses. ### PowerShell surface conventions @@ -190,19 +213,36 @@ exist to keep large lists from flickering. ## Data model -Schema lives in `Database/CreateDatabase.txt` (SQL Server 2017; LocalDB/Express are fine, attached -`.mdf` mode is the recommended setup). All primary keys are `uniqueidentifier` generated in C# with -`Guid.NewGuid()`, never by the database. +Schema lives in `ImageStore/Database/SqliteSchema.cs`, as the statements +`New-ImageStoreDatabase` executes. `ImageStore.Migrator` compiles that same file **by linked +compile item** — a migrated database and a freshly created one have to match, and two copies would +eventually disagree. Change it in one place only. + +All primary keys are Guids generated in C# with `Guid.NewGuid()`, never by the database, and stored +as 16-byte `BLOB`. | Table | Notes | |---|---| | `Folder` | Root of an image library. `CompareImageWith` controls comparison scope. `IsSealed` marks read-only libraries. | | `IgnoredDirectory` | Per-folder exclusions, optionally recursive. | | `Extension` | One row per file extension; `IsImage` and `Ignored` drive what gets hashed. | -| `File` | `Path` + `FileName` + `ExtensionId` relative to the folder; `ImageHash binary(40)` (pHash), `Sha1Hash binary(20)`, `FileState`, `ImageComparedThreshold`. | +| `File` | `Path` + `FileName` + `ExtensionId` relative to the folder; `ImageHash` (pHash, 40 bytes), `Sha1Hash` (20 bytes), `FileState`, `ImageComparedThreshold`. | | `SameFile` | Rows grouped by shared `Sha1Hash`; `IsIgnored` hides a row from review. | | `SimilarFile` | Pair `File1Id`/`File2Id` with `DifferenceDegree` and `IgnoredMode`. | +Two schema details are load-bearing: + +- **`COLLATE NOCASE`** on `Path`, `FileName`, `Extension`, `Name` and `Directory`. Without it path + comparison turns case-sensitive, which contradicts both the Windows file system and the + in-memory `StringComparer.OrdinalIgnoreCase` use, and the UNIQUE indexes stop catching `.JPG` + versus `.jpg`. Note the limit: SQLite's NOCASE folds ASCII only, so accented names still compare + exactly. +- **Cascades are asymmetric, deliberately.** `File`, `IgnoredDirectory` and `SameFile` cascade from + their parent; `SimilarFile` does not. That is why `FileHelper` deletes `SimilarFile` rows by hand + before deleting files, and why `RemoveFolderCmdlet` deletes only `Folder` and `SimilarFile` and + lets the rest cascade. `DatabaseConnection` sets `PRAGMA foreign_keys = ON` explicitly rather + than relying on the provider default. + Enums that must stay in sync with the stored `int` values: - `FileState` — `New = 0`, `NotImage = 1`, `NotReadable = 2`, `SizeZero = 254`, `Computed = 255`. @@ -243,21 +283,24 @@ Use it for anything risky: nothing here can be *run* outside Windows, so CI is t feedback before merging. Each release carries two assets: `ImageStore-.zip` (the whole publish tree minus `.pdb`) and -`ImageStore-Database-.zip` (the empty `.mdf`/`.ldf` and `CreateDatabase.txt`). The database is -deliberately separate — its contents are identical in every release and are only needed once, when -setting up a project. The module archive is copied wholesale rather than as flat `*.dll`, because -`deps.json` and `runtimes/win-*/native/` have to keep their layout. +`ImageStore-Migrator-.zip`. The migrator is separate because it is needed once per library, by +the few users coming from Sql Server, and it carries the whole SqlClient dependency tree the module +no longer has. There is no empty-database asset any more — `New-ImageStoreDatabase` creates the +file. The module archive is copied wholesale rather than as flat `*.dll`, because `deps.json` and +`runtimes/win-*/native/` have to keep their layout. The "Verify build output" step guards the package in both directions: - **Nothing missing.** `ImageStore.dll`, `ImageStore.deps.json`, both `Shipwreck.Phash*` dlls, - `Microsoft.Data.SqlClient.dll`, and — checked recursively — the native SNI library. + `Microsoft.Data.Sqlite.dll`, the three `SQLitePCLRaw.*` dlls, and the native `e_sqlite3.dll` + under `runtimes\win-x64\native` — which also proves the non-Windows trim did not take it. - **Nothing extra.** No `System.Management.Automation.dll`, `pwrshplugin.dll`, `PowerShell.Core.Instrumentation.dll`, `libpsl-native.*` or `getfilesiginforedist.dll`. These belong to the PowerShell host and are kept out by `ExcludeAssets="runtime;native"`; the check is - the net for a regression, since an oversized package otherwise looks perfectly healthy. Both - halves of this have already caught a real mistake — v2026.08.15.1 shipped the reference assembly, - and the native payload leaked in during the .NET 10 upgrade. + the net for a regression, since an oversized package otherwise looks perfectly healthy. It also + asserts no `Microsoft.Data.SqlClient.dll` reaches the module. Both halves have already caught + real mistakes — v2026.08.15.1 shipped the reference assembly, and the native payload leaked in + during the .NET 10 upgrade. The workflow does not touch `AssemblyInfo.cs`: the dll stays at `1.0.0.0` and the version lives only in the tag, release title, and asset name. diff --git a/Database/CreateDatabase.txt b/Database/CreateDatabase.txt deleted file mode 100644 index 204b3ec..0000000 --- a/Database/CreateDatabase.txt +++ /dev/null @@ -1,321 +0,0 @@ -USE [master] -GO -/****** Object: Database [C:\Database\DataStore.MDF] Script Date: 12/15/2018 10:31:25 PM ******/ -CREATE DATABASE [C:\Database\DataStore.MDF] - CONTAINMENT = NONE - ON PRIMARY -( NAME = N'DataStore', FILENAME = N'C:\Database\DataStore.mdf' , SIZE = 4096KB , MAXSIZE = UNLIMITED, FILEGROWTH = 65536KB ) - LOG ON -( NAME = N'DataStore_log', FILENAME = N'C:\Database\DataStore_log.ldf' , SIZE = 4096KB , MAXSIZE = 2048GB , FILEGROWTH = 65536KB ) -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET COMPATIBILITY_LEVEL = 100 -GO -IF (1 = FULLTEXTSERVICEPROPERTY('IsFullTextInstalled')) -begin -EXEC [C:\Database\DataStore.MDF].[dbo].[sp_fulltext_database] @action = 'enable' -end -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET ANSI_NULL_DEFAULT OFF -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET ANSI_NULLS OFF -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET ANSI_PADDING OFF -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET ANSI_WARNINGS OFF -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET ARITHABORT OFF -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET AUTO_CLOSE ON -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET AUTO_SHRINK ON -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET AUTO_UPDATE_STATISTICS ON -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET CURSOR_CLOSE_ON_COMMIT OFF -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET CURSOR_DEFAULT GLOBAL -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET CONCAT_NULL_YIELDS_NULL OFF -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET NUMERIC_ROUNDABORT OFF -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET QUOTED_IDENTIFIER OFF -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET RECURSIVE_TRIGGERS OFF -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET DISABLE_BROKER -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET AUTO_UPDATE_STATISTICS_ASYNC OFF -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET DATE_CORRELATION_OPTIMIZATION OFF -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET TRUSTWORTHY OFF -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET ALLOW_SNAPSHOT_ISOLATION OFF -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET PARAMETERIZATION SIMPLE -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET READ_COMMITTED_SNAPSHOT OFF -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET HONOR_BROKER_PRIORITY OFF -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET RECOVERY SIMPLE -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET MULTI_USER -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET PAGE_VERIFY CHECKSUM -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET DB_CHAINING OFF -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET FILESTREAM( NON_TRANSACTED_ACCESS = OFF ) -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET TARGET_RECOVERY_TIME = 60 SECONDS -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET DELAYED_DURABILITY = DISABLED -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET QUERY_STORE = OFF -GO -USE [C:\Database\DataStore.MDF] -GO -/****** Object: Table [dbo].[Extension] Script Date: 12/15/2018 10:31:25 PM ******/ -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -CREATE TABLE [dbo].[Extension]( - [Id] [uniqueidentifier] NOT NULL, - [Extension] [nvarchar](256) NOT NULL, - [IsImage] [bit] NOT NULL, - [Ignored] [bit] NOT NULL, -PRIMARY KEY CLUSTERED -( - [Id] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] -) ON [PRIMARY] -GO -/****** Object: Table [dbo].[File] Script Date: 12/15/2018 10:31:25 PM ******/ -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -CREATE TABLE [dbo].[File]( - [Id] [uniqueidentifier] NOT NULL, - [FolderId] [uniqueidentifier] NOT NULL, - [Path] [nvarchar](256) NOT NULL, - [FileName] [nvarchar](256) NOT NULL, - [ExtensionId] [uniqueidentifier] NOT NULL, - [ImageHash] [binary](40) NULL, - [Sha1Hash] [binary](20) NULL, - [FileSize] [int] NOT NULL, - [FileState] [int] NOT NULL, - [ImageComparedThreshold] [real] NOT NULL, -PRIMARY KEY CLUSTERED -( - [Id] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] -) ON [PRIMARY] -GO -/****** Object: Table [dbo].[Folder] Script Date: 12/15/2018 10:31:25 PM ******/ -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -CREATE TABLE [dbo].[Folder]( - [Id] [uniqueidentifier] NOT NULL, - [Name] [nvarchar](256) NOT NULL, - [Path] [nvarchar](256) NOT NULL, - [CompareImageWith] [int] NOT NULL, - [IsSealed] [bit] NOT NULL, -PRIMARY KEY CLUSTERED -( - [Id] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] -) ON [PRIMARY] -GO -/****** Object: Table [dbo].[IgnoredDirectory] Script Date: 12/15/2018 10:31:25 PM ******/ -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -CREATE TABLE [dbo].[IgnoredDirectory]( - [Id] [uniqueidentifier] NOT NULL, - [FolderId] [uniqueidentifier] NOT NULL, - [Directory] [nvarchar](256) NOT NULL, - [IsSubDirectoryIncluded] [bit] NOT NULL, -PRIMARY KEY CLUSTERED -( - [Id] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] -) ON [PRIMARY] -GO -/****** Object: Table [dbo].[SameFile] Script Date: 12/15/2018 10:31:26 PM ******/ -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -CREATE TABLE [dbo].[SameFile]( - [Id] [uniqueidentifier] NOT NULL, - [Sha1Hash] [binary](20) NOT NULL, - [FileId] [uniqueidentifier] NOT NULL, - [IsIgnored] [bit] NOT NULL, - CONSTRAINT [PK__SameFile__3214EC07AA2C19A1] PRIMARY KEY CLUSTERED -( - [Id] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] -) ON [PRIMARY] -GO -/****** Object: Table [dbo].[SimilarFile] Script Date: 12/15/2018 10:31:26 PM ******/ -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -CREATE TABLE [dbo].[SimilarFile]( - [Id] [uniqueidentifier] NOT NULL, - [File1Id] [uniqueidentifier] NOT NULL, - [File2Id] [uniqueidentifier] NOT NULL, - [DifferenceDegree] [real] NOT NULL, - [IgnoredMode] [int] NOT NULL, - CONSTRAINT [PK_SimilarFile] PRIMARY KEY CLUSTERED -( - [Id] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] -) ON [PRIMARY] -GO -SET ANSI_PADDING ON -GO -/****** Object: Index [IX_Extension] Script Date: 12/15/2018 10:31:26 PM ******/ -CREATE UNIQUE NONCLUSTERED INDEX [IX_Extension] ON [dbo].[Extension] -( - [Extension] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] -GO -/****** Object: Index [IX_File_FolderId] Script Date: 12/15/2018 10:31:26 PM ******/ -CREATE NONCLUSTERED INDEX [IX_File_FolderId] ON [dbo].[File] -( - [FolderId] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] -GO -SET ANSI_PADDING ON -GO -/****** Object: Index [IX_File_FolderIdPath] Script Date: 12/15/2018 10:31:26 PM ******/ -CREATE NONCLUSTERED INDEX [IX_File_FolderIdPath] ON [dbo].[File] -( - [FolderId] ASC, - [Path] ASC, - [FileName] ASC, - [ExtensionId] ASC, - [FileState] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] -GO -/****** Object: Index [IX_File_ForComparing] Script Date: 12/15/2018 10:31:26 PM ******/ -CREATE NONCLUSTERED INDEX [IX_File_ForComparing] ON [dbo].[File] -( - [ImageComparedThreshold] ASC, - [FileState] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] -GO -SET ANSI_PADDING ON -GO -/****** Object: Index [IX_Folder_Name] Script Date: 12/15/2018 10:31:26 PM ******/ -CREATE UNIQUE NONCLUSTERED INDEX [IX_Folder_Name] ON [dbo].[Folder] -( - [Name] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] -GO -SET ANSI_PADDING ON -GO -/****** Object: Index [IX_Folder_Path] Script Date: 12/15/2018 10:31:26 PM ******/ -CREATE NONCLUSTERED INDEX [IX_Folder_Path] ON [dbo].[Folder] -( - [Path] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] -GO -/****** Object: Index [IX_IgnoredDirectory_FolderId] Script Date: 12/15/2018 10:31:26 PM ******/ -CREATE NONCLUSTERED INDEX [IX_IgnoredDirectory_FolderId] ON [dbo].[IgnoredDirectory] -( - [FolderId] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] -GO -SET ANSI_PADDING ON -GO -/****** Object: Index [IX_IgnoredDirectory_FolderIdDirectory] Script Date: 12/15/2018 10:31:26 PM ******/ -CREATE UNIQUE NONCLUSTERED INDEX [IX_IgnoredDirectory_FolderIdDirectory] ON [dbo].[IgnoredDirectory] -( - [FolderId] ASC, - [Directory] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] -GO -/****** Object: Index [IX_SameFile_FileId] Script Date: 12/15/2018 10:31:26 PM ******/ -CREATE UNIQUE NONCLUSTERED INDEX [IX_SameFile_FileId] ON [dbo].[SameFile] -( - [FileId] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] -GO -SET ANSI_PADDING ON -GO -/****** Object: Index [IX_SameFile_Sha1Hash] Script Date: 12/15/2018 10:31:26 PM ******/ -CREATE NONCLUSTERED INDEX [IX_SameFile_Sha1Hash] ON [dbo].[SameFile] -( - [Sha1Hash] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] -GO -/****** Object: Index [IX_SimilarFile] Script Date: 12/15/2018 10:31:26 PM ******/ -CREATE NONCLUSTERED INDEX [IX_SimilarFile] ON [dbo].[SimilarFile] -( - [DifferenceDegree] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] -GO -/****** Object: Index [IX_SimilarFile_File1] Script Date: 12/15/2018 10:31:26 PM ******/ -CREATE NONCLUSTERED INDEX [IX_SimilarFile_File1] ON [dbo].[SimilarFile] -( - [File1Id] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] -GO -/****** Object: Index [IX_SimilarFile_File2] Script Date: 12/15/2018 10:31:26 PM ******/ -CREATE NONCLUSTERED INDEX [IX_SimilarFile_File2] ON [dbo].[SimilarFile] -( - [File2Id] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] -GO -ALTER TABLE [dbo].[File] ADD DEFAULT ((-1)) FOR [FileSize] -GO -ALTER TABLE [dbo].[File] ADD CONSTRAINT [DF_File_ImageComparedThreshold] DEFAULT ((0)) FOR [ImageComparedThreshold] -GO -ALTER TABLE [dbo].[File] WITH CHECK ADD CONSTRAINT [FK_File_ToExtension] FOREIGN KEY([ExtensionId]) -REFERENCES [dbo].[Extension] ([Id]) -ON DELETE CASCADE -GO -ALTER TABLE [dbo].[File] CHECK CONSTRAINT [FK_File_ToExtension] -GO -ALTER TABLE [dbo].[File] WITH CHECK ADD CONSTRAINT [FK_File_ToFolder] FOREIGN KEY([FolderId]) -REFERENCES [dbo].[Folder] ([Id]) -ON DELETE CASCADE -GO -ALTER TABLE [dbo].[File] CHECK CONSTRAINT [FK_File_ToFolder] -GO -ALTER TABLE [dbo].[IgnoredDirectory] WITH CHECK ADD CONSTRAINT [FK_IgnoredDirectory_ToFolder] FOREIGN KEY([FolderId]) -REFERENCES [dbo].[Folder] ([Id]) -ON DELETE CASCADE -GO -ALTER TABLE [dbo].[IgnoredDirectory] CHECK CONSTRAINT [FK_IgnoredDirectory_ToFolder] -GO -ALTER TABLE [dbo].[SameFile] WITH CHECK ADD CONSTRAINT [FK_SameFile_ToFile] FOREIGN KEY([FileId]) -REFERENCES [dbo].[File] ([Id]) -ON DELETE CASCADE -GO -ALTER TABLE [dbo].[SameFile] CHECK CONSTRAINT [FK_SameFile_ToFile] -GO -ALTER TABLE [dbo].[SimilarFile] WITH CHECK ADD CONSTRAINT [FK_SimilarFile_File] FOREIGN KEY([File1Id]) -REFERENCES [dbo].[File] ([Id]) -GO -ALTER TABLE [dbo].[SimilarFile] CHECK CONSTRAINT [FK_SimilarFile_File] -GO -ALTER TABLE [dbo].[SimilarFile] WITH CHECK ADD CONSTRAINT [FK_SimilarFile_File1] FOREIGN KEY([File2Id]) -REFERENCES [dbo].[File] ([Id]) -GO -ALTER TABLE [dbo].[SimilarFile] CHECK CONSTRAINT [FK_SimilarFile_File1] -GO -USE [master] -GO -ALTER DATABASE [C:\Database\DataStore.MDF] SET READ_WRITE -GO diff --git a/Database/DataStore.mdf b/Database/DataStore.mdf deleted file mode 100644 index 96ebc66..0000000 Binary files a/Database/DataStore.mdf and /dev/null differ diff --git a/Database/DataStore_log.ldf b/Database/DataStore_log.ldf deleted file mode 100644 index f374bd0..0000000 Binary files a/Database/DataStore_log.ldf and /dev/null differ diff --git a/ImageStore.Migrator/ImageStore.Migrator.csproj b/ImageStore.Migrator/ImageStore.Migrator.csproj new file mode 100644 index 0000000..6d4a88b --- /dev/null +++ b/ImageStore.Migrator/ImageStore.Migrator.csproj @@ -0,0 +1,28 @@ + + + + Exe + + net10.0 + SecretNest.ImageStore.Migrator + ImageStore.Migrator + disable + true + + + + + + + + + + + + + + diff --git a/ImageStore.Migrator/Program.cs b/ImageStore.Migrator/Program.cs new file mode 100644 index 0000000..a54ff1a --- /dev/null +++ b/ImageStore.Migrator/Program.cs @@ -0,0 +1,199 @@ +using Microsoft.Data.Sqlite; +using Microsoft.Data.SqlClient; +using SecretNest.ImageStore.Database; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; + +namespace SecretNest.ImageStore.Migrator +{ + /// + /// Copies an ImageStore library from Sql Server into a SQLite file. + /// + /// + /// Shipped separately from the module because it is needed exactly once per + /// library, and because it is the only piece that still has to talk to Sql + /// Server. + /// + static class Program + { + static int Main(string[] args) + { + string source = null; + string target = null; + + for (var i = 0; i < args.Length - 1; i++) + { + switch (args[i].ToLowerInvariant()) + { + case "--source": + case "-s": + source = args[++i]; + break; + case "--target": + case "-t": + target = args[++i]; + break; + } + } + + if (string.IsNullOrEmpty(source) || string.IsNullOrEmpty(target)) + { + Console.Error.WriteLine("ImageStore migration tool - Sql Server to SQLite"); + Console.Error.WriteLine(); + Console.Error.WriteLine(" ImageStore.Migrator --source --target "); + Console.Error.WriteLine(); + Console.Error.WriteLine("Example:"); + Console.Error.WriteLine(" ImageStore.Migrator \\"); + Console.Error.WriteLine(" --source \"server=(LocalDB)\\MSSQLLocalDB;AttachDbFilename=D:\\DataStore.mdf;Integrated Security=True\" \\"); + Console.Error.WriteLine(" --target D:\\library.db"); + return 2; + } + + try + { + Migrate(source, target); + return 0; + } + catch (Exception ex) + { + Console.Error.WriteLine(); + Console.Error.WriteLine("Migration failed: " + ex.Message); + return 1; + } + } + + static void Migrate(string source, string targetPath) + { + var fullPath = Path.GetFullPath(targetPath); + + if (File.Exists(fullPath)) + throw new IOException("Target file exists already: " + fullPath); + + Console.WriteLine("Source: " + source); + Console.WriteLine("Target: " + fullPath); + Console.WriteLine(); + + var stopwatch = Stopwatch.StartNew(); + + using (var sqlServer = new SqlConnection(source)) + using (var sqlite = new SqliteConnection(new SqliteConnectionStringBuilder { DataSource = fullPath }.ToString())) + { + sqlServer.Open(); + sqlite.Open(); + + CreateSchema(sqlite); + + var total = 0L; + foreach (var table in SqliteSchema.TablesInDependencyOrder) + total += CopyTable(sqlServer, sqlite, table); + + stopwatch.Stop(); + Console.WriteLine(); + Console.WriteLine($"Done. {total:N0} rows in {stopwatch.Elapsed:hh\\:mm\\:ss}."); + Console.WriteLine(); + Console.WriteLine("Open it with: Open-ImageStoreDatabase \"" + fullPath + "\""); + } + } + + static void CreateSchema(SqliteConnection sqlite) + { + using (var transaction = sqlite.BeginTransaction()) + { + foreach (var statement in SqliteSchema.CreationStatements) + { + using (var command = sqlite.CreateCommand()) + { + command.Transaction = transaction; + command.CommandText = statement; + command.ExecuteNonQuery(); + } + } + transaction.Commit(); + } + Console.WriteLine("Schema created."); + } + + /// + /// Columns per table, matching both schemas. Written out rather than + /// discovered so that a column added on one side without the other shows up + /// as an error here instead of as silently missing data. + /// + static readonly Dictionary Columns = new Dictionary + { + ["Folder"] = new[] { "Id", "Name", "Path", "CompareImageWith", "IsSealed" }, + ["Extension"] = new[] { "Id", "Extension", "IsImage", "Ignored" }, + ["File"] = new[] { "Id", "FolderId", "Path", "FileName", "ExtensionId", "ImageHash", "Sha1Hash", "FileSize", "FileState", "ImageComparedThreshold" }, + ["IgnoredDirectory"] = new[] { "Id", "FolderId", "Directory", "IsSubDirectoryIncluded" }, + ["SameFile"] = new[] { "Id", "Sha1Hash", "FileId", "IsIgnored" }, + ["SimilarFile"] = new[] { "Id", "File1Id", "File2Id", "DifferenceDegree", "IgnoredMode" }, + }; + + static long CopyTable(SqlConnection sqlServer, SqliteConnection sqlite, string table) + { + var columns = Columns[table]; + var columnList = string.Join(",", Array.ConvertAll(columns, c => "[" + c + "]")); + var valueList = string.Join(",", Array.ConvertAll(columns, c => "@" + c)); + + Console.Write($"{table,-18} "); + + var rows = 0L; + + //One transaction for the whole table. SQLite commits per statement + //otherwise, which turns a million inserts into a million fsyncs. + using (var transaction = sqlite.BeginTransaction()) + using (var insert = sqlite.CreateCommand()) + { + insert.Transaction = transaction; + insert.CommandText = $"INSERT INTO [{table}] ({columnList}) VALUES ({valueList})"; + + foreach (var column in columns) + insert.Parameters.Add(new SqliteParameter("@" + column, DBNull.Value)); + + using (var select = new SqlCommand($"SELECT {columnList} FROM [{table}]", sqlServer) { CommandTimeout = 0 }) + using (var reader = select.ExecuteReader(System.Data.CommandBehavior.SequentialAccess)) + { + while (reader.Read()) + { + for (var i = 0; i < columns.Length; i++) + insert.Parameters[i].Value = Convert(reader[i]); + + insert.ExecuteNonQuery(); + rows++; + + if (rows % 20000 == 0) + Console.Write("."); + } + } + + transaction.Commit(); + } + + Console.WriteLine($" {rows:N0} rows"); + return rows; + } + + /// + /// Maps a Sql Server value onto its SQLite representation. + /// + static object Convert(object value) + { + if (value == null || value == DBNull.Value) + return DBNull.Value; + + //uniqueidentifier becomes the same 16 bytes the module writes. Storing + //the string form instead would produce a database that opens fine and + //then matches nothing. + if (value is Guid guid) + return guid.ToByteArray(); + + //bit becomes 0 or 1. + if (value is bool flag) + return flag ? 1 : 0; + + //real, int, binary and nvarchar map across as they are. + return value; + } + } +} diff --git a/ImageStore.sln b/ImageStore.sln index 2048ae4..7180214 100644 --- a/ImageStore.sln +++ b/ImageStore.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 15.0.27703.2035 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageStore", "ImageStore\ImageStore.csproj", "{8FA09B96-6FA3-4BBF-9E30-CC720D43C041}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageStore.Migrator", "ImageStore.Migrator\ImageStore.Migrator.csproj", "{3D6C1B84-9E27-4C55-9F53-2A7E51B0C6D2}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "doc", "doc", "{9C14AD78-9262-4C54-927A-A58133A7CF94}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cmdlet", "cmdlet", "{20CDC5AD-D882-474E-AFEB-B26828597677}" @@ -44,6 +46,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Database", "Database", "{EF ProjectSection(SolutionItems) = preProject doc\cmdlet\Database\CloseDatabase.md = doc\cmdlet\Database\CloseDatabase.md doc\cmdlet\Database\CompressDatabase.md = doc\cmdlet\Database\CompressDatabase.md + doc\cmdlet\Database\NewDatabase.md = doc\cmdlet\Database\NewDatabase.md doc\cmdlet\Database\OpenDatabase.md = doc\cmdlet\Database\OpenDatabase.md EndProjectSection EndProject @@ -130,13 +133,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Database", "Database", "{FD8A01E1-83A0-4C10-B69E-7CED2BD8ED1E}" - ProjectSection(SolutionItems) = preProject - Database\CreateDatabase.txt = Database\CreateDatabase.txt - Database\DataStore.mdf = Database\DataStore.mdf - Database\DataStore_log.ldf = Database\DataStore_log.ldf - EndProjectSection -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -147,6 +143,10 @@ Global {8FA09B96-6FA3-4BBF-9E30-CC720D43C041}.Debug|Any CPU.Build.0 = Debug|Any CPU {8FA09B96-6FA3-4BBF-9E30-CC720D43C041}.Release|Any CPU.ActiveCfg = Release|Any CPU {8FA09B96-6FA3-4BBF-9E30-CC720D43C041}.Release|Any CPU.Build.0 = Release|Any CPU + {3D6C1B84-9E27-4C55-9F53-2A7E51B0C6D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3D6C1B84-9E27-4C55-9F53-2A7E51B0C6D2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3D6C1B84-9E27-4C55-9F53-2A7E51B0C6D2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3D6C1B84-9E27-4C55-9F53-2A7E51B0C6D2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ImageStore/Database/CompressDatabaseCmdlet.cs b/ImageStore/Database/CompressDatabaseCmdlet.cs index 3d9211b..f0a246d 100644 --- a/ImageStore/Database/CompressDatabaseCmdlet.cs +++ b/ImageStore/Database/CompressDatabaseCmdlet.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Management.Automation; using System.Text; @@ -16,7 +16,10 @@ protected override void ProcessRecord() { var connection = DatabaseConnection.Current; - using (SqlCommand command = new SqlCommand("DECLARE @dbName VARCHAR(500); SELECT @dbName = DB_NAME(); DBCC SHRINKDATABASE(@dbName)")) + //VACUUM rebuilds the database file, reclaiming pages freed by deletes. + //It cannot run inside a transaction and needs free disk space roughly + //equal to the database size while it works. + using (SqliteCommand command = new SqliteCommand("VACUUM")) { command.Connection = connection; command.CommandTimeout = 0; diff --git a/ImageStore/Database/NewDatabaseCmdlet.cs b/ImageStore/Database/NewDatabaseCmdlet.cs new file mode 100644 index 0000000..3f35a46 --- /dev/null +++ b/ImageStore/Database/NewDatabaseCmdlet.cs @@ -0,0 +1,126 @@ +using Microsoft.Data.Sqlite; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Text; +using System.Threading.Tasks; + +namespace SecretNest.ImageStore.Database +{ + [Cmdlet(VerbsCommon.New, "ImageStoreDatabase")] + [Alias("NewDatabase")] + public class NewDatabaseCmdlet : PSCmdlet + { + [Parameter(ValueFromPipelineByPropertyName = true, Position = 0, ValueFromPipeline = true, Mandatory = true)] + public string Path { get; set; } + + /// + /// Replaces an existing file. Without this, an existing path is an error + /// rather than something to overwrite - the file is somebody's library. + /// + [Parameter(ValueFromPipelineByPropertyName = true, Position = 1)] + public SwitchParameter Force { get; set; } + + protected override void ProcessRecord() + { + //Resolved against the PowerShell location rather than the process working + //directory; see the same note in Open-ImageStoreDatabase. + var fullPath = GetUnresolvedProviderPathFromPSPath(Path); + + if (System.IO.File.Exists(fullPath)) + { + if (!Force.IsPresent) + { + ThrowTerminatingError(new ErrorRecord( + new IOException("File exists already. Use -Force to replace it."), + "ImageStore New Database", ErrorCategory.ResourceExists, fullPath)); + return; + } + + //Close first: the file being replaced may be the one currently open, + //and on Windows it cannot be deleted while a handle is held. + DatabaseConnection.Close(); + + try + { + DeleteDatabaseFiles(fullPath); + } + catch (Exception ex) + { + ThrowTerminatingError(new ErrorRecord(ex, + "ImageStore New Database", ErrorCategory.WriteError, fullPath)); + return; + } + } + + var directory = System.IO.Path.GetDirectoryName(fullPath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + ThrowTerminatingError(new ErrorRecord( + new DirectoryNotFoundException("Directory of the database file is not found."), + "ImageStore New Database", ErrorCategory.ObjectNotFound, directory)); + return; + } + + //ReadWriteCreate is the default, and here creating is the point. + var connectionString = DatabaseConnection.BuildConnectionString(fullPath); + + try + { + DatabaseConnection.Connect(connectionString); + CreateSchema(); + } + catch (SqliteException ex) + { + DatabaseConnection.Close(); + ThrowTerminatingError(new ErrorRecord(ex, + "ImageStore New Database", ErrorCategory.WriteError, fullPath)); + return; + } + + //Left open on purpose: creating a database is followed by using it, so + //this stands in for a subsequent Open-ImageStoreDatabase. + WriteInformation("Database created and opened: " + DatabaseConnection.CurrentPath, + new string[] { "Database", "New", "Open" }); + } + + void CreateSchema() + { + var connection = DatabaseConnection.Current; + + using (var transaction = connection.BeginTransaction()) + { + foreach (var statement in SqliteSchema.CreationStatements) + { + using (var command = connection.CreateCommand()) + { + command.Transaction = transaction; + command.CommandText = statement; + command.ExecuteNonQuery(); + } + } + + transaction.Commit(); + } + } + + /// + /// Removes the database and the journal files that live beside it. Leaving a + /// stale -wal behind would let SQLite recover content from the database this + /// one replaced. + /// + static void DeleteDatabaseFiles(string fullPath) + { + System.IO.File.Delete(fullPath); + + foreach (var suffix in new[] { "-wal", "-shm", "-journal" }) + { + var companion = fullPath + suffix; + if (System.IO.File.Exists(companion)) + System.IO.File.Delete(companion); + } + } + } +} diff --git a/ImageStore/Database/OpenDatabaseCmdlet.cs b/ImageStore/Database/OpenDatabaseCmdlet.cs index 550d13d..80844c8 100644 --- a/ImageStore/Database/OpenDatabaseCmdlet.cs +++ b/ImageStore/Database/OpenDatabaseCmdlet.cs @@ -1,4 +1,5 @@ -using System; +using Microsoft.Data.Sqlite; +using System; using System.Collections.Generic; using System.Linq; using System.Management.Automation; @@ -7,16 +8,61 @@ namespace SecretNest.ImageStore.Database { - [Cmdlet(VerbsCommon.Open, "ImageStoreDatabase")] + [Cmdlet(VerbsCommon.Open, "ImageStoreDatabase", DefaultParameterSetName = PathParameterSet)] [Alias("OpenDatabase")] - public class OpenDatabaseCmdlet : Cmdlet + public class OpenDatabaseCmdlet : PSCmdlet { - [Parameter(ValueFromPipelineByPropertyName = true, Position = 0, ValueFromPipeline = true, Mandatory = true)] + internal const string PathParameterSet = "Path"; + internal const string ConnectionStringParameterSet = "ConnectionString"; + + [Parameter(ParameterSetName = PathParameterSet, ValueFromPipelineByPropertyName = true, + Position = 0, ValueFromPipeline = true, Mandatory = true)] + public string Path { get; set; } + + /// + /// For settings the path alone cannot express, such as Mode=ReadOnly. + /// + [Parameter(ParameterSetName = ConnectionStringParameterSet, ValueFromPipelineByPropertyName = true, + Position = 0, ValueFromPipeline = true, Mandatory = true)] public string ConnectionString { get; set; } protected override void ProcessRecord() { - DatabaseConnection.Connect(ConnectionString); + string connectionString; + + if (ParameterSetName == ConnectionStringParameterSet) + { + connectionString = ConnectionString; + } + else + { + //Resolved against the PowerShell location, not the process working + //directory. The two are routinely different, and using the wrong one + //would silently open or create a file somewhere unexpected. + var fullPath = GetUnresolvedProviderPathFromPSPath(Path); + + if (!System.IO.File.Exists(fullPath)) + { + ThrowTerminatingError(new ErrorRecord( + new System.IO.FileNotFoundException("Database file is not found.", fullPath), + "ImageStore Open Database", ErrorCategory.ObjectNotFound, fullPath)); + return; + } + + //Mode=ReadWrite rather than the default ReadWriteCreate: opening a + //path that does not exist should fail, not quietly produce an empty + //database with no tables that then fails on the first real query. + connectionString = new SqliteConnectionStringBuilder + { + DataSource = fullPath, + Mode = SqliteOpenMode.ReadWrite + }.ToString(); + } + + DatabaseConnection.Connect(connectionString); + + WriteInformation("Database opened: " + DatabaseConnection.CurrentPath, + new string[] { "Database", "Open" }); } } } diff --git a/ImageStore/Database/SqliteSchema.cs b/ImageStore/Database/SqliteSchema.cs new file mode 100644 index 0000000..6706c1b --- /dev/null +++ b/ImageStore/Database/SqliteSchema.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SecretNest.ImageStore.Database +{ + /// + /// The database schema, translated from the Sql Server script this project used + /// before SQLite. Single source of truth: the migration tool compiles this same + /// file by link, so a new database and a migrated one cannot drift apart. + /// + /// + /// Type mapping from the original: + /// uniqueidentifier -> BLOB, holding the 16 bytes of Guid.ToByteArray(). + /// bit -> INTEGER, 0 or 1. + /// real -> REAL. SQLite stores 8 bytes where Sql Server stored 4, + /// which only widens the value. + /// binary(n) -> BLOB. + /// nvarchar(256) -> TEXT COLLATE NOCASE. + /// + /// The NOCASE collation keeps path and name comparison case-insensitive, matching + /// both Sql Server's default collation and the Windows file system. Note that + /// SQLite's NOCASE only folds ASCII A-Z, so accented characters compare exactly; + /// this is a real difference from Sql Server for non-English file names. + /// + /// Cascades are reproduced exactly as they were, including the asymmetry: + /// File, IgnoredDirectory and SameFile cascade from their parent, but SimilarFile + /// does not. Code that deletes files therefore has to clear SimilarFile rows + /// itself - see FileHelper - and that stays true here. + /// + static class SqliteSchema + { + /// + /// Statements to create an empty database, in dependency order. + /// + internal static IReadOnlyList CreationStatements { get; } = new[] + { + @"CREATE TABLE [Folder]( + [Id] BLOB NOT NULL PRIMARY KEY, + [Name] TEXT NOT NULL COLLATE NOCASE, + [Path] TEXT NOT NULL COLLATE NOCASE, + [CompareImageWith] INTEGER NOT NULL, + [IsSealed] INTEGER NOT NULL)", + + @"CREATE UNIQUE INDEX [IX_Folder_Name] ON [Folder]([Name])", + @"CREATE INDEX [IX_Folder_Path] ON [Folder]([Path])", + + @"CREATE TABLE [Extension]( + [Id] BLOB NOT NULL PRIMARY KEY, + [Extension] TEXT NOT NULL COLLATE NOCASE, + [IsImage] INTEGER NOT NULL, + [Ignored] INTEGER NOT NULL)", + + @"CREATE UNIQUE INDEX [IX_Extension] ON [Extension]([Extension])", + + @"CREATE TABLE [File]( + [Id] BLOB NOT NULL PRIMARY KEY, + [FolderId] BLOB NOT NULL REFERENCES [Folder]([Id]) ON DELETE CASCADE, + [Path] TEXT NOT NULL COLLATE NOCASE, + [FileName] TEXT NOT NULL COLLATE NOCASE, + [ExtensionId] BLOB NOT NULL REFERENCES [Extension]([Id]) ON DELETE CASCADE, + [ImageHash] BLOB NULL, + [Sha1Hash] BLOB NULL, + [FileSize] INTEGER NOT NULL DEFAULT (-1), + [FileState] INTEGER NOT NULL, + [ImageComparedThreshold] REAL NOT NULL DEFAULT (0))", + + @"CREATE INDEX [IX_File_FolderId] ON [File]([FolderId])", + @"CREATE INDEX [IX_File_FolderIdPath] ON [File]([FolderId],[Path],[FileName],[ExtensionId],[FileState])", + @"CREATE INDEX [IX_File_ForComparing] ON [File]([ImageComparedThreshold],[FileState])", + + @"CREATE TABLE [IgnoredDirectory]( + [Id] BLOB NOT NULL PRIMARY KEY, + [FolderId] BLOB NOT NULL REFERENCES [Folder]([Id]) ON DELETE CASCADE, + [Directory] TEXT NOT NULL COLLATE NOCASE, + [IsSubDirectoryIncluded] INTEGER NOT NULL)", + + @"CREATE INDEX [IX_IgnoredDirectory_FolderId] ON [IgnoredDirectory]([FolderId])", + @"CREATE UNIQUE INDEX [IX_IgnoredDirectory_FolderIdDirectory] ON [IgnoredDirectory]([FolderId],[Directory])", + + @"CREATE TABLE [SameFile]( + [Id] BLOB NOT NULL PRIMARY KEY, + [Sha1Hash] BLOB NOT NULL, + [FileId] BLOB NOT NULL REFERENCES [File]([Id]) ON DELETE CASCADE, + [IsIgnored] INTEGER NOT NULL)", + + @"CREATE UNIQUE INDEX [IX_SameFile_FileId] ON [SameFile]([FileId])", + @"CREATE INDEX [IX_SameFile_Sha1Hash] ON [SameFile]([Sha1Hash])", + + //No cascade on either foreign key, matching the original schema. + @"CREATE TABLE [SimilarFile]( + [Id] BLOB NOT NULL PRIMARY KEY, + [File1Id] BLOB NOT NULL REFERENCES [File]([Id]), + [File2Id] BLOB NOT NULL REFERENCES [File]([Id]), + [DifferenceDegree] REAL NOT NULL, + [IgnoredMode] INTEGER NOT NULL)", + + @"CREATE INDEX [IX_SimilarFile] ON [SimilarFile]([DifferenceDegree])", + @"CREATE INDEX [IX_SimilarFile_File1] ON [SimilarFile]([File1Id])", + @"CREATE INDEX [IX_SimilarFile_File2] ON [SimilarFile]([File2Id])" + }; + + /// + /// Names of every table, in an order safe for inserting with foreign keys on. + /// Used by the migration tool. + /// + internal static IReadOnlyList TablesInDependencyOrder { get; } = new[] + { + "Folder", "Extension", "File", "IgnoredDirectory", "SameFile", "SimilarFile" + }; + } +} diff --git a/ImageStore/DatabaseShared/DatabaseConnection.cs b/ImageStore/DatabaseShared/DatabaseConnection.cs index d1e05fd..80086ad 100644 --- a/ImageStore/DatabaseShared/DatabaseConnection.cs +++ b/ImageStore/DatabaseShared/DatabaseConnection.cs @@ -1,6 +1,6 @@ -using System; +using Microsoft.Data.Sqlite; +using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -9,9 +9,12 @@ namespace SecretNest.ImageStore { public static class DatabaseConnection { - static SqlConnection _current; + static SqliteConnection _current; + static string _currentPath; - public static SqlConnection Current + static readonly object _lock = new object(); + + public static SqliteConnection Current { get { @@ -22,21 +25,101 @@ public static SqlConnection Current } } + /// + /// Path of the open database file, or null when none is open. + /// + public static string CurrentPath + { + get { return _currentPath; } + } + + /// + /// Whether a database is currently open. Unlike this + /// does not throw, so cleanup paths can ask without guarding. + /// + public static bool IsOpen + { + get { return _current != null; } + } + + internal static string BuildConnectionString(string path) + { + return new SqliteConnectionStringBuilder + { + DataSource = path + }.ToString(); + } + internal static void Connect(string connectionString) { Close(); - _current = new SqlConnection(connectionString); - _current.Open(); + + var connection = new SqliteConnection(connectionString); + connection.Open(); + + //Foreign keys are enforced per connection. Microsoft.Data.Sqlite turns + //them on by default, but the cascades this schema relies on are not + //optional - removing a folder deletes its files only through them - so + //set it explicitly rather than trusting a provider default. + Execute(connection, "PRAGMA foreign_keys = ON"); + + //Write-ahead logging: readers do not block the writer, which matters + //while Measure-ImageStoreFiles streams results in from worker threads. + //Silently ignored on filesystems that cannot support it, which is the + //reason not to verify the result here. + Execute(connection, "PRAGMA journal_mode = WAL"); + + _current = connection; + _currentPath = connection.DataSource; + } + + static void Execute(SqliteConnection connection, string commandText) + { + using (var command = connection.CreateCommand()) + { + command.CommandText = commandText; + command.ExecuteNonQuery(); + } } + /// + /// Closes the current database, if any. Safe to call repeatedly and from the + /// process-exit handler, which may race a Close-ImageStoreDatabase already + /// running on the pipeline thread. + /// internal static void Close() { - if (_current != null) + lock (_lock) { - _current.Close(); - _current.Dispose(); + if (_current == null) + return; + + var connection = _current; + _current = null; + _currentPath = null; + + try + { + //Checkpoint so the -wal and -shm files are folded back into the + //database and removed, rather than left beside it. + Execute(connection, "PRAGMA optimize"); + Execute(connection, "PRAGMA wal_checkpoint(TRUNCATE)"); + } + catch (SqliteException) + { + //Nothing useful to do while shutting down. SQLite recovers from + //an unclean close on its own, and this can run during process + //exit where reporting is no longer possible. + } + + connection.Close(); + connection.Dispose(); + + //Without this the handle stays in the connection pool and the file + //remains locked, so -wal/-shm linger and the database cannot be + //moved or deleted until the process ends. + SqliteConnection.ClearAllPools(); } - _current = null; } } } diff --git a/ImageStore/DatabaseShared/ModuleLifetime.cs b/ImageStore/DatabaseShared/ModuleLifetime.cs new file mode 100644 index 0000000..cd19e7a --- /dev/null +++ b/ImageStore/DatabaseShared/ModuleLifetime.cs @@ -0,0 +1,53 @@ +using System; +using System.Management.Automation; + +namespace SecretNest.ImageStore +{ + /// + /// Closes the database when the module goes away. + /// + /// + /// Two separate hooks are needed, because neither covers the other: + /// + /// OnRemove fires for Remove-ImageStoreDatabase's module, that is Remove-Module. + /// It does not fire when the host simply exits. + /// + /// ProcessExit fires on a normal host exit, which is how a session actually + /// ends most of the time. + /// + /// Both funnel into DatabaseConnection.Close, which is idempotent and locked, so + /// firing twice - or racing a Close-ImageStoreDatabase on the pipeline thread - + /// is harmless. Neither hook runs if the process is killed; SQLite recovers from + /// that on its own, the cost being the -wal and -shm files left behind. + /// + public class ModuleLifetime : IModuleAssemblyInitializer, IModuleAssemblyCleanup + { + static bool _processExitHooked; + static readonly object _lock = new object(); + + public void OnImport() + { + lock (_lock) + { + //Importing twice in one session must not stack handlers. The + //assembly is never unloaded, so this static survives Remove-Module + //followed by a fresh Import-Module. + if (_processExitHooked) + return; + + AppDomain.CurrentDomain.ProcessExit += OnProcessExit; + _processExitHooked = true; + } + } + + public void OnRemove(PSModuleInfo psModuleInfo) + { + DatabaseConnection.Close(); + } + + static void OnProcessExit(object sender, EventArgs e) + { + DatabaseConnection.Close(); + } + } +} diff --git a/ImageStore/DatabaseShared/SqlServerLikeValueBuilder.cs b/ImageStore/DatabaseShared/SqlServerLikeValueBuilder.cs deleted file mode 100644 index c0070e3..0000000 --- a/ImageStore/DatabaseShared/SqlServerLikeValueBuilder.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace SecretNest.ImageStore -{ - static class SqlServerLikeValueBuilder - { - internal static string EscapeAndInclude(string value) - { - return "%" + Escape(value) + "%"; - } - - internal static string Escape(string value) - { - return value.Replace("'", "''").Replace("[", "[[]").Replace("%", "[%]").Replace("_", "[_]"); - } - - internal static string EscapeForEquals(string value) - { - return value.Replace("'", "''"); - } - } -} diff --git a/ImageStore/DatabaseShared/SqliteLikeValueBuilder.cs b/ImageStore/DatabaseShared/SqliteLikeValueBuilder.cs new file mode 100644 index 0000000..c5fd6fe --- /dev/null +++ b/ImageStore/DatabaseShared/SqliteLikeValueBuilder.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SecretNest.ImageStore +{ + /// + /// Prepares values for LIKE comparison. + /// + /// + /// This replaces the Sql Server version, which differed in two ways that both + /// mattered. + /// + /// Sql Server escapes LIKE wildcards with character classes - "[%]" for a literal + /// percent. SQLite's LIKE has no character classes at all, so those brackets would + /// be matched literally while the percent inside them kept its wildcard meaning: + /// the escaping would not merely fail, it would turn an exact search into a + /// wildcard one. SQLite instead needs an explicit ESCAPE clause, which is what + /// is for. + /// + /// The old version also doubled single quotes. That was a real bug rather than a + /// dialect difference: these values are bound as parameters and never parsed as + /// SQL, so doubling corrupted them. Searching for a file named "Don't.jpg" could + /// not match, because the stored name contains one quote and the search value had + /// been rewritten to two. Nothing here doubles quotes. + /// + /// Case sensitivity needs no handling: SQLite's LIKE already ignores case for + /// ASCII, which is what Sql Server's default collation did. + /// + static class SqliteLikeValueBuilder + { + /// + /// Backslash, chosen because it cannot appear in a Windows file or directory + /// name and so is never itself the thing being searched for. + /// + internal const string EscapeCharacter = "\\"; + + /// + /// Append to any LIKE comparison built from these values. + /// + internal const string EscapeClause = " escape '\\'"; + + /// + /// Escapes the LIKE metacharacters, leaving the value otherwise untouched. + /// + internal static string Escape(string value) + { + //Backslash first, or the escapes added below would be escaped again. + return value + .Replace(EscapeCharacter, EscapeCharacter + EscapeCharacter) + .Replace("%", EscapeCharacter + "%") + .Replace("_", EscapeCharacter + "_"); + } + + /// + /// Escapes and wraps in wildcards, for a "contains" search. + /// + internal static string EscapeAndInclude(string value) + { + return "%" + Escape(value) + "%"; + } + } +} diff --git a/ImageStore/DatabaseShared/SqliteParameterExtensions.cs b/ImageStore/DatabaseShared/SqliteParameterExtensions.cs new file mode 100644 index 0000000..eba1108 --- /dev/null +++ b/ImageStore/DatabaseShared/SqliteParameterExtensions.cs @@ -0,0 +1,64 @@ +using Microsoft.Data.Sqlite; +using System; + +namespace SecretNest.ImageStore +{ + /// + /// Parameter binding helpers. + /// + /// + /// The Guid overloads exist for a reason worth stating plainly: binding a Guid + /// directly makes Microsoft.Data.Sqlite store it as 36-character TEXT. The schema + /// declares BLOB and every other write goes through ToByteArray, so a single + /// direct binding would store a value that no lookup ever matches - and it fails + /// silently, as a query returning nothing rather than an error. + /// + /// Routing every binding through here means that conversion is written once. + /// + static class SqliteParameterExtensions + { + internal static void AddGuid(this SqliteParameterCollection parameters, string name, Guid value) + { + parameters.Add(new SqliteParameter(name, SqliteType.Blob) { Value = value.ToByteArray() }); + } + + internal static void AddGuid(this SqliteParameterCollection parameters, string name, Guid? value) + { + parameters.Add(new SqliteParameter(name, SqliteType.Blob) + { + Value = value.HasValue ? (object)value.Value.ToByteArray() : DBNull.Value + }); + } + + internal static void AddBlob(this SqliteParameterCollection parameters, string name, byte[] value) + { + parameters.Add(new SqliteParameter(name, SqliteType.Blob) + { + Value = value ?? (object)DBNull.Value + }); + } + + internal static void AddText(this SqliteParameterCollection parameters, string name, string value) + { + parameters.Add(new SqliteParameter(name, SqliteType.Text) + { + Value = value ?? (object)DBNull.Value + }); + } + + internal static void AddInt(this SqliteParameterCollection parameters, string name, int value) + { + parameters.Add(new SqliteParameter(name, SqliteType.Integer) { Value = value }); + } + + internal static void AddBool(this SqliteParameterCollection parameters, string name, bool value) + { + parameters.Add(new SqliteParameter(name, SqliteType.Integer) { Value = value ? 1 : 0 }); + } + + internal static void AddReal(this SqliteParameterCollection parameters, string name, float value) + { + parameters.Add(new SqliteParameter(name, SqliteType.Real) { Value = value }); + } + } +} diff --git a/ImageStore/DatabaseShared/WhereCauseBuilder.cs b/ImageStore/DatabaseShared/WhereCauseBuilder.cs index a9cf85c..c9cadf4 100644 --- a/ImageStore/DatabaseShared/WhereCauseBuilder.cs +++ b/ImageStore/DatabaseShared/WhereCauseBuilder.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -10,8 +10,8 @@ namespace SecretNest.ImageStore.DatabaseShared class WhereCauseBuilder { - SqlParameterCollection parameters; List whereCauses; bool allMet; - public WhereCauseBuilder(SqlParameterCollection parameters, bool allMet = true) + SqliteParameterCollection parameters; List whereCauses; bool allMet; + public WhereCauseBuilder(SqliteParameterCollection parameters, bool allMet = true) { this.parameters = parameters; whereCauses = new List(); @@ -51,7 +51,7 @@ internal void AddBinaryComparingCause(string columnName, byte[] value, bool isNu else if (value != null) { whereCauses.Add(string.Format("[{0}] = @{0}", columnName)); - parameters.Add(new SqlParameter("@" + columnName, System.Data.SqlDbType.Binary, length) { Value = value }); + parameters.AddBlob("@" + columnName, value); } } @@ -60,7 +60,7 @@ internal void AddUniqueIdentifierComparingCause(string columnName, Guid? value) if (value.HasValue) { whereCauses.Add(string.Format("[{0}] = @{0}", columnName)); - parameters.Add(new SqlParameter("@" + columnName, System.Data.SqlDbType.UniqueIdentifier) { Value = value.Value }); + parameters.AddGuid("@" + columnName, value.Value); } } @@ -93,7 +93,7 @@ internal void AddIntComparingCause(string columnName, int? value, string paramet if (value.HasValue) { whereCauses.Add(string.Format("[{0}] = @{1}", columnName, parameterName)); - parameters.Add(new SqlParameter("@" + parameterName, System.Data.SqlDbType.Int) { Value = value.Value }); + parameters.AddInt("@" + parameterName, value.Value); } } @@ -102,20 +102,20 @@ internal void AddIntComparingCause(string columnName, int? value, int? greaterOr if (value.HasValue) { whereCauses.Add(string.Format("[{0}] = @{0}", columnName)); - parameters.Add(new SqlParameter("@" + columnName, System.Data.SqlDbType.Int) { Value = value.Value }); + parameters.AddInt("@" + columnName, value.Value); } else { if (greaterOrEqual.HasValue) { whereCauses.Add(string.Format("[{0}] >= @GreaterOrEqual{0}", columnName)); - parameters.Add(new SqlParameter("@GreaterOrEqual" + columnName, System.Data.SqlDbType.Int) { Value = greaterOrEqual.Value }); + parameters.AddInt("@GreaterOrEqual" + columnName, greaterOrEqual.Value); } if (lessOrEqual.HasValue) { whereCauses.Add(string.Format("[{0}] <= @LessOrEqual{0}", columnName)); - parameters.Add(new SqlParameter("@LessOrEqual" + columnName, System.Data.SqlDbType.Int) { Value = lessOrEqual.Value }); + parameters.AddInt("@LessOrEqual" + columnName, lessOrEqual.Value); } } } @@ -125,7 +125,7 @@ internal void AddBitComparingCause(string columnName, bool? value) if (value.HasValue) { whereCauses.Add(string.Format("[{0}] = @{0}", columnName)); - parameters.Add(new SqlParameter("@" + columnName, System.Data.SqlDbType.Bit) { Value = value.Value }); + parameters.AddBool("@" + columnName, value.Value); } } @@ -134,20 +134,20 @@ internal void AddRealComparingCause(string columnName, float? value, float? grea if (value.HasValue) { whereCauses.Add(string.Format("[{0}] = @{0}", columnName)); - parameters.Add(new SqlParameter("@" + columnName, System.Data.SqlDbType.Real) { Value = value }); + parameters.AddReal("@" + columnName, value.Value); } else { if (greaterOrEqual.HasValue) { whereCauses.Add(string.Format("[{0}] >= @GreaterOrEqual{0}", columnName)); - parameters.Add(new SqlParameter("@GreaterOrEqual" + columnName, System.Data.SqlDbType.Real) { Value = greaterOrEqual.Value }); + parameters.AddReal("@GreaterOrEqual" + columnName, greaterOrEqual.Value); } if (lessOrEqual.HasValue) { whereCauses.Add(string.Format("[{0}] <= @LessOrEqual{0}", columnName)); - parameters.Add(new SqlParameter("@LessOrEqual" + columnName, System.Data.SqlDbType.Real) { Value = lessOrEqual.Value }); + parameters.AddReal("@LessOrEqual" + columnName, lessOrEqual.Value); } } } @@ -169,27 +169,23 @@ void AddStringComparingCause(string columnName, string value, StringPropertyComp { if (comparingModes == StringPropertyComparingModes.Contains) { - whereCauses.Add(string.Format("[{0}] like @{1}", columnName, parameterName)); - parameters.Add(new SqlParameter("@" + parameterName, System.Data.SqlDbType.NVarChar, length * 3 + 2) - { Value = SqlServerLikeValueBuilder.EscapeAndInclude(value) }); + whereCauses.Add(string.Format("[{0}] like @{1}" + SqliteLikeValueBuilder.EscapeClause, columnName, parameterName)); + parameters.AddText("@" + parameterName, SqliteLikeValueBuilder.EscapeAndInclude(value)); } else if (comparingModes == StringPropertyComparingModes.Equals) { whereCauses.Add(string.Format("[{0}] = @{1}", columnName, parameterName)); - parameters.Add(new SqlParameter("@" + parameterName, System.Data.SqlDbType.NVarChar, length * 3) - { Value = SqlServerLikeValueBuilder.EscapeForEquals(value) }); + parameters.AddText("@" + parameterName, value); } else if (comparingModes == StringPropertyComparingModes.StartsWith) { - whereCauses.Add(string.Format("[{0}] like @{1}", columnName, parameterName)); - parameters.Add(new SqlParameter("@" + parameterName, System.Data.SqlDbType.NVarChar, length * 3 + 1) - { Value = SqlServerLikeValueBuilder.Escape(value) + "%" }); + whereCauses.Add(string.Format("[{0}] like @{1}" + SqliteLikeValueBuilder.EscapeClause, columnName, parameterName)); + parameters.AddText("@" + parameterName, SqliteLikeValueBuilder.Escape(value) + "%"); } else if (comparingModes == StringPropertyComparingModes.EndsWith) { - whereCauses.Add(string.Format("[{0}] like @{1}", columnName, parameterName)); - parameters.Add(new SqlParameter("@" + parameterName, System.Data.SqlDbType.NVarChar, length * 3 + 1) - { Value = "%" + SqlServerLikeValueBuilder.Escape(value) }); + whereCauses.Add(string.Format("[{0}] like @{1}" + SqliteLikeValueBuilder.EscapeClause, columnName, parameterName)); + parameters.AddText("@" + parameterName, "%" + SqliteLikeValueBuilder.Escape(value)); } else { diff --git a/ImageStore/Extension/AddExtensionCmdlet.cs b/ImageStore/Extension/AddExtensionCmdlet.cs index abf5c37..38fdbbc 100644 --- a/ImageStore/Extension/AddExtensionCmdlet.cs +++ b/ImageStore/Extension/AddExtensionCmdlet.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Management.Automation; using System.Text; @@ -31,14 +31,14 @@ protected override void ProcessRecord() var connection = DatabaseConnection.Current; var id = Guid.NewGuid(); - using (var command = new SqlCommand("Insert into [Extension] values(@Id, @Extension, @IsImage, @Ignored)")) + using (var command = new SqliteCommand("Insert into [Extension] values(@Id, @Extension, @IsImage, @Ignored)")) { command.Connection = connection; command.CommandTimeout = 0; - command.Parameters.Add(new SqlParameter("@Id", System.Data.SqlDbType.UniqueIdentifier) { Value = id }); - command.Parameters.Add(new SqlParameter("@Extension", System.Data.SqlDbType.NVarChar, 256) { Value = Extension }); - command.Parameters.Add(new SqlParameter("@IsImage", System.Data.SqlDbType.Bit) { Value = IsImage }); - command.Parameters.Add(new SqlParameter("@Ignored", System.Data.SqlDbType.Bit) { Value = Ignored }); + command.Parameters.AddGuid("@Id", id); + command.Parameters.AddText("@Extension", Extension); + command.Parameters.AddBool("@IsImage", IsImage); + command.Parameters.AddBool("@Ignored", Ignored); if (command.ExecuteNonQuery() > 0) { diff --git a/ImageStore/Extension/ExtensionHelper.cs b/ImageStore/Extension/ExtensionHelper.cs index a346a83..a4669f6 100644 --- a/ImageStore/Extension/ExtensionHelper.cs +++ b/ImageStore/Extension/ExtensionHelper.cs @@ -1,7 +1,7 @@ using SecretNest.ImageStore.Extension; using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -13,7 +13,7 @@ static class ExtensionHelper internal static IEnumerable GetAllExtensions() { var connection = DatabaseConnection.Current; - using (var command = new SqlCommand("Select [Id],[Extension],[IsImage],[Ignored] from [Extension]")) + using (var command = new SqliteCommand("Select [Id],[Extension],[IsImage],[Ignored] from [Extension]")) { command.Connection = connection; command.CommandTimeout = 0; @@ -21,11 +21,11 @@ internal static IEnumerable GetAllExtensions() { while (reader.Read()) { - ImageStoreExtension line = new ImageStoreExtension((Guid)reader[0]) + ImageStoreExtension line = new ImageStoreExtension(reader.GetGuid(0)) { - Extension = (string)reader[1], - IsImage = (bool)reader[2], - Ignored = (bool)reader[3] + Extension = reader.GetString(1), + IsImage = reader.GetBoolean(2), + Ignored = reader.GetBoolean(3) }; yield return line; } @@ -37,19 +37,19 @@ internal static IEnumerable GetAllExtensions() internal static string GetExtensionName(Guid id, out bool isImage, out bool ignored) { var connection = DatabaseConnection.Current; - using (var command = new SqlCommand("Select [Extension],[IsImage],[Ignored] from [Extension] Where [Id]=@Id")) + using (var command = new SqliteCommand("Select [Extension],[IsImage],[Ignored] from [Extension] Where [Id]=@Id")) { command.Connection = connection; command.CommandTimeout = 0; - command.Parameters.Add(new SqlParameter("@Id", System.Data.SqlDbType.UniqueIdentifier) { Value = id }); + command.Parameters.AddGuid("@Id", id); using (var reader = command.ExecuteReader(System.Data.CommandBehavior.SequentialAccess)) { string result; if (reader.Read()) { - result = (string)reader[0]; - isImage = (bool)reader[1]; - ignored = (bool)reader[2]; + result = reader.GetString(0); + isImage = reader.GetBoolean(1); + ignored = reader.GetBoolean(2); } else { diff --git a/ImageStore/Extension/FindExtensionCmdlet.cs b/ImageStore/Extension/FindExtensionCmdlet.cs index 7c4bbc6..abf110e 100644 --- a/ImageStore/Extension/FindExtensionCmdlet.cs +++ b/ImageStore/Extension/FindExtensionCmdlet.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Management.Automation; using System.Text; @@ -22,21 +22,21 @@ protected override void ProcessRecord() throw new ArgumentNullException(nameof(Extension)); var connection = DatabaseConnection.Current; - using (var command = new SqlCommand("Select [Id],[Extension],[IsImage],[Ignored] from [Extension] Where [Extension]=@Extension")) + using (var command = new SqliteCommand("Select [Id],[Extension],[IsImage],[Ignored] from [Extension] Where [Extension]=@Extension")) { command.Connection = connection; command.CommandTimeout = 0; - command.Parameters.Add(new SqlParameter("@Extension", System.Data.SqlDbType.NVarChar, 256) { Value = Extension }); + command.Parameters.AddText("@Extension", Extension); using (var reader = command.ExecuteReader(System.Data.CommandBehavior.SequentialAccess)) { if (reader.Read()) { - ImageStoreExtension line = new ImageStoreExtension((Guid)reader[0]) + ImageStoreExtension line = new ImageStoreExtension(reader.GetGuid(0)) { - Extension = (string)reader[1], - IsImage = (bool)reader[2], - Ignored = (bool)reader[3] + Extension = reader.GetString(1), + IsImage = reader.GetBoolean(2), + Ignored = reader.GetBoolean(3) }; WriteObject(line); } diff --git a/ImageStore/Extension/GetExtensionCmdlet.cs b/ImageStore/Extension/GetExtensionCmdlet.cs index d415780..1046ae9 100644 --- a/ImageStore/Extension/GetExtensionCmdlet.cs +++ b/ImageStore/Extension/GetExtensionCmdlet.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Management.Automation; using System.Text; @@ -19,11 +19,11 @@ public class GetExtensionCmdlet : Cmdlet protected override void ProcessRecord() { var connection = DatabaseConnection.Current; - using (var command = new SqlCommand("Select [Extension],[IsImage],[Ignored] from [Extension] Where [Id]=@Id")) + using (var command = new SqliteCommand("Select [Extension],[IsImage],[Ignored] from [Extension] Where [Id]=@Id")) { command.Connection = connection; command.CommandTimeout = 0; - command.Parameters.Add(new SqlParameter("@Id", System.Data.SqlDbType.UniqueIdentifier) { Value = Id }); + command.Parameters.AddGuid("@Id", Id); using (var reader = command.ExecuteReader(System.Data.CommandBehavior.SequentialAccess)) { @@ -31,9 +31,9 @@ protected override void ProcessRecord() { ImageStoreExtension line = new ImageStoreExtension(Id) { - Extension = (string)reader[0], - IsImage = (bool)reader[1], - Ignored = (bool)reader[2] + Extension = reader.GetString(0), + IsImage = reader.GetBoolean(1), + Ignored = reader.GetBoolean(2) }; WriteObject(line); } diff --git a/ImageStore/Extension/RemoveExtensionCmdlet.cs b/ImageStore/Extension/RemoveExtensionCmdlet.cs index 8e8e999..d190895 100644 --- a/ImageStore/Extension/RemoveExtensionCmdlet.cs +++ b/ImageStore/Extension/RemoveExtensionCmdlet.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Management.Automation; using System.Management.Automation.Runspaces; @@ -26,11 +26,11 @@ protected override void ProcessRecord() var connection = DatabaseConnection.Current; - using (var commandCreateTable = new SqlCommand("Create Table #tempFileId ([Id] uniqueidentifier)")) - using (var commandSelect = new SqlCommand("insert into #tempFileId select [Id] from [File] where [ExtensionId]=@ExtensionId")) - using (var commandDeleteSimilar = new SqlCommand("Delete from [SimilarFile] where [File1Id] in (select [Id] from #tempFileId) or [File2Id] in (select [Id] from #tempFileId)")) - using (var commandDropTable = new SqlCommand("Drop Table #tempFileId")) - using (var commandDeleteExtension = new SqlCommand("Delete from [Extension] where [Id]=@Id")) + using (var commandCreateTable = new SqliteCommand("Create temp table tempFileId ([Id] BLOB)")) + using (var commandSelect = new SqliteCommand("insert into tempFileId select [Id] from [File] where [ExtensionId]=@ExtensionId")) + using (var commandDeleteSimilar = new SqliteCommand("Delete from [SimilarFile] where [File1Id] in (select [Id] from tempFileId) or [File2Id] in (select [Id] from tempFileId)")) + using (var commandDropTable = new SqliteCommand("Drop Table tempFileId")) + using (var commandDeleteExtension = new SqliteCommand("Delete from [Extension] where [Id]=@Id")) using (var transation = connection.BeginTransaction()) { commandCreateTable.Connection = connection; @@ -41,7 +41,7 @@ protected override void ProcessRecord() commandSelect.Connection = connection; commandSelect.CommandTimeout = 0; commandSelect.Transaction = transation; - commandSelect.Parameters.Add(new SqlParameter("@ExtensionId", System.Data.SqlDbType.UniqueIdentifier) { Value = Id }); + commandSelect.Parameters.AddGuid("@ExtensionId", Id); if (commandSelect.ExecuteNonQuery() != 0) { @@ -52,7 +52,7 @@ protected override void ProcessRecord() if (SimilarFile.LoadImageHelper.cachePath != null) { - using (var commandReadId = new SqlCommand("Select [Id] from #tempFileId")) + using (var commandReadId = new SqliteCommand("Select [Id] from tempFileId")) { commandReadId.Connection = connection; commandReadId.CommandTimeout = 0; @@ -60,7 +60,7 @@ protected override void ProcessRecord() using (var reader = commandReadId.ExecuteReader(System.Data.CommandBehavior.SequentialAccess)) { while (reader.Read()) - SimilarFile.LoadImageHelper.RemoveCache((Guid)reader[0]); + SimilarFile.LoadImageHelper.RemoveCache(reader.GetGuid(0)); reader.Close(); } } @@ -73,7 +73,7 @@ protected override void ProcessRecord() commandDeleteExtension.Connection = connection; commandDeleteExtension.CommandTimeout = 0; - commandDeleteExtension.Parameters.Add(new SqlParameter("@Id", System.Data.SqlDbType.UniqueIdentifier) { Value = Id }); + commandDeleteExtension.Parameters.AddGuid("@Id", Id); int result = commandDeleteExtension.ExecuteNonQuery(); if (result == 0) diff --git a/ImageStore/Extension/SearchExtensionCmdlet.cs b/ImageStore/Extension/SearchExtensionCmdlet.cs index 5aaa02f..e34aca8 100644 --- a/ImageStore/Extension/SearchExtensionCmdlet.cs +++ b/ImageStore/Extension/SearchExtensionCmdlet.cs @@ -1,7 +1,7 @@ using SecretNest.ImageStore.DatabaseShared; using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Management.Automation; using System.Text; @@ -30,7 +30,7 @@ protected override void ProcessRecord() { var connection = DatabaseConnection.Current; - using (var command = new SqlCommand("Select [Id],[Extension],[IsImage],[Ignored] from [Extension]")) + using (var command = new SqliteCommand("Select [Id],[Extension],[IsImage],[Ignored] from [Extension]")) { command.Connection = connection; command.CommandTimeout = 0; @@ -48,11 +48,11 @@ protected override void ProcessRecord() { while(reader.Read()) { - ImageStoreExtension line = new ImageStoreExtension((Guid)reader[0]) + ImageStoreExtension line = new ImageStoreExtension(reader.GetGuid(0)) { - Extension = (string)reader[1], - IsImage = (bool)reader[2], - Ignored = (bool)reader[3] + Extension = reader.GetString(1), + IsImage = reader.GetBoolean(2), + Ignored = reader.GetBoolean(3) }; result.Add(line); } diff --git a/ImageStore/Extension/UpdateExtensionCmdlet.cs b/ImageStore/Extension/UpdateExtensionCmdlet.cs index 2cc076c..8dd3ac1 100644 --- a/ImageStore/Extension/UpdateExtensionCmdlet.cs +++ b/ImageStore/Extension/UpdateExtensionCmdlet.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Management.Automation; using System.Text; @@ -22,14 +22,14 @@ protected override void ProcessRecord() var connection = DatabaseConnection.Current; - using (var command = new SqlCommand("Update [Extension] Set Extension=@Extension, IsImage=@IsImage, [Ignored]=@Ignored where [Id]=@Id")) + using (var command = new SqliteCommand("Update [Extension] Set Extension=@Extension, IsImage=@IsImage, [Ignored]=@Ignored where [Id]=@Id")) { command.Connection = connection; command.CommandTimeout = 0; - command.Parameters.Add(new SqlParameter("@Id", System.Data.SqlDbType.UniqueIdentifier) { Value = Extension.Id }); - command.Parameters.Add(new SqlParameter("@Extension", System.Data.SqlDbType.NVarChar, 256) { Value = Extension.Extension }); - command.Parameters.Add(new SqlParameter("@IsImage", System.Data.SqlDbType.Bit) { Value = Extension.IsImage }); - command.Parameters.Add(new SqlParameter("@Ignored", System.Data.SqlDbType.Bit) { Value = Extension.Ignored }); + command.Parameters.AddGuid("@Id", Extension.Id); + command.Parameters.AddText("@Extension", Extension.Extension); + command.Parameters.AddBool("@IsImage", Extension.IsImage); + command.Parameters.AddBool("@Ignored", Extension.Ignored); if (command.ExecuteNonQuery() == 0) { diff --git a/ImageStore/File/AddFileCmdlet.cs b/ImageStore/File/AddFileCmdlet.cs index 6367f42..014bfd2 100644 --- a/ImageStore/File/AddFileCmdlet.cs +++ b/ImageStore/File/AddFileCmdlet.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Management.Automation; using System.Text; @@ -44,15 +44,15 @@ protected override void ProcessRecord() var connection = DatabaseConnection.Current; var id = Guid.NewGuid(); - using (var command = new SqlCommand("Insert into [File] values(@Id, @FolderId, @Path, @FileName, @ExtensionId, null, null, -1, 0, 0)")) + using (var command = new SqliteCommand("Insert into [File] values(@Id, @FolderId, @Path, @FileName, @ExtensionId, null, null, -1, 0, 0)")) { command.Connection = connection; command.CommandTimeout = 0; - command.Parameters.Add(new SqlParameter("@Id", System.Data.SqlDbType.UniqueIdentifier) { Value = id }); - command.Parameters.Add(new SqlParameter("@FolderId", System.Data.SqlDbType.UniqueIdentifier) { Value = Folder.Id }); - command.Parameters.Add(new SqlParameter("@Path", System.Data.SqlDbType.NVarChar, 256) { Value = Path }); - command.Parameters.Add(new SqlParameter("@FileName", System.Data.SqlDbType.NVarChar, 256) { Value = FileName }); - command.Parameters.Add(new SqlParameter("@ExtensionId", System.Data.SqlDbType.UniqueIdentifier) { Value = Extension.Id }); + command.Parameters.AddGuid("@Id", id); + command.Parameters.AddGuid("@FolderId", Folder.Id); + command.Parameters.AddText("@Path", Path); + command.Parameters.AddText("@FileName", FileName); + command.Parameters.AddGuid("@ExtensionId", Extension.Id); if (command.ExecuteNonQuery() > 0) { diff --git a/ImageStore/File/FileHelper.cs b/ImageStore/File/FileHelper.cs index addad1b..acb893c 100644 --- a/ImageStore/File/FileHelper.cs +++ b/ImageStore/File/FileHelper.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -18,22 +18,22 @@ internal static string GetFullFilePath(string folder, string path, string fileNa internal static string GetFileName(Guid fileId, out string folderPath, out string path, out string fileNameWithoutPath, out bool isFolderSealed, out Guid folderId) { var connection = DatabaseConnection.Current; - using (var command = new SqlCommand("Select [Folder].[Path],[File].[Path],[File].[FileName],[Extension].[Extension],[Folder].[IsSealed],[File].[FolderId] from [File] inner join [Folder] on [File].[FolderId]=[Folder].[Id] inner join [Extension] on [File].[ExtensionId]=[Extension].[Id] where [File].[Id]=@Id")) + using (var command = new SqliteCommand("Select [Folder].[Path],[File].[Path],[File].[FileName],[Extension].[Extension],[Folder].[IsSealed],[File].[FolderId] from [File] inner join [Folder] on [File].[FolderId]=[Folder].[Id] inner join [Extension] on [File].[ExtensionId]=[Extension].[Id] where [File].[Id]=@Id")) { command.Connection = connection; command.CommandTimeout = 0; - command.Parameters.Add(new SqlParameter("@Id", System.Data.SqlDbType.UniqueIdentifier) { Value = fileId }); + command.Parameters.AddGuid("@Id", fileId); using (var reader = command.ExecuteReader(System.Data.CommandBehavior.SequentialAccess)) { string result; if (reader.Read()) { - folderPath = (string)reader[0]; - path = (string)reader[1]; - fileNameWithoutPath = (string)reader[2] + "." + (string)reader[3]; - isFolderSealed = (bool)reader[4]; - folderId = (Guid)reader[5]; + folderPath = reader.GetString(0); + path = reader.GetString(1); + fileNameWithoutPath = reader.GetString(2) + "." + reader.GetString(3); + isFolderSealed = reader.GetBoolean(4); + folderId = reader.GetGuid(5); if (!folderPath.EndsWith(DirectorySeparatorString.Value)) folderPath += DirectorySeparatorString.Value; if (path == "") result = folderPath + fileNameWithoutPath; @@ -58,21 +58,21 @@ internal static IEnumerable GetAllFilesWithoutData(Guid folderId { var connection = DatabaseConnection.Current; var text = " [Id],[Path],[FileName],[ExtensionId] from [File] where [FolderId]=@FolderId order by [Path],[FileName],[ExtensionId]"; - using (var command = new SqlCommand()) + using (var command = new SqliteCommand()) { command.Connection = connection; command.CommandTimeout = 0; + //LIMIT goes last, after the order by already inside text. + command.CommandText = "SELECT" + text; if (top != null) - command.CommandText = "SELECT TOP " + top.Value.ToString() + text; - else - command.CommandText = "SELECT" + text; - command.Parameters.Add(new SqlParameter("@FolderId", System.Data.SqlDbType.UniqueIdentifier) { Value = folderId }); + command.CommandText += " limit " + top.Value.ToString(); + command.Parameters.AddGuid("@FolderId", folderId); using (var reader = command.ExecuteReader(System.Data.CommandBehavior.SequentialAccess)) { while(reader.Read()) { - ImageStoreFile line = new ImageStoreFile((Guid)reader[0], folderId, (string)reader[1], (string)reader[2], (Guid)reader[3]); + ImageStoreFile line = new ImageStoreFile(reader.GetGuid(0), folderId, reader.GetString(1), reader.GetString(2), reader.GetGuid(3)); yield return line; } reader.Close(); @@ -84,11 +84,7 @@ internal static IEnumerable GetAllFilesWithoutData(Guid folderId bool onlyNew, bool includingComputed, bool includingNotImage, bool includingNotReadable, bool includingSizeZero) { var connection = DatabaseConnection.Current; - string text; - if (top != null) - text = "SELECT TOP " + top.Value.ToString(); - else - text = "SELECT"; + string text = "SELECT"; text += " [Id],[Path],[FileName],[ExtensionId],[FileState] from [File] where [FolderId]=@FolderId and "; @@ -117,20 +113,24 @@ internal static IEnumerable GetAllFilesWithoutData(Guid folderId text += " order by [Path],[FileName],[ExtensionId]"; - using (var command = new SqlCommand(text)) + //LIMIT goes last, after the where and order by clauses built above. + if (top != null) + text += " limit " + top.Value.ToString(); + + using (var command = new SqliteCommand(text)) { command.Connection = connection; command.CommandTimeout = 0; - command.Parameters.Add(new SqlParameter("@FolderId", System.Data.SqlDbType.UniqueIdentifier) { Value = folderId }); + command.Parameters.AddGuid("@FolderId", folderId); using (var reader = command.ExecuteReader(System.Data.CommandBehavior.SequentialAccess)) { while (reader.Read()) { - ImageStoreFile line = new ImageStoreFile((Guid)reader[0], folderId, (string)reader[1], (string)reader[2], (Guid)reader[3]) + ImageStoreFile line = new ImageStoreFile(reader.GetGuid(0), folderId, reader.GetString(1), reader.GetString(2), reader.GetGuid(3)) { - FileStateCode = (int)reader[4] + FileStateCode = reader.GetInt32(4) }; yield return line; } @@ -142,18 +142,18 @@ internal static IEnumerable GetAllFilesWithoutData(Guid folderId internal static bool Delete(Guid id) { var connection = DatabaseConnection.Current; - using (var commandFile = new SqlCommand("Delete from [File] where [Id]=@Id")) - using (var commandSimilar = new SqlCommand("Delete from [SimilarFile] where [File1Id]=@Id or [File2Id]=@Id")) + using (var commandFile = new SqliteCommand("Delete from [File] where [Id]=@Id")) + using (var commandSimilar = new SqliteCommand("Delete from [SimilarFile] where [File1Id]=@Id or [File2Id]=@Id")) using (var transation = connection.BeginTransaction()) { commandFile.Connection = connection; commandFile.CommandTimeout = 0; commandFile.Transaction = transation; - commandFile.Parameters.Add(new SqlParameter("@Id", System.Data.SqlDbType.UniqueIdentifier) { Value = id }); + commandFile.Parameters.AddGuid("@Id", id); commandSimilar.Connection = connection; commandSimilar.CommandTimeout = 0; commandSimilar.Transaction = transation; - commandSimilar.Parameters.Add(new SqlParameter("@Id", System.Data.SqlDbType.UniqueIdentifier) { Value = id }); + commandSimilar.Parameters.AddGuid("@Id", id); commandSimilar.ExecuteNonQuery(); @@ -172,24 +172,24 @@ internal static bool Delete(Guid id) internal static void Delete(IEnumerable> operations) { var connection = DatabaseConnection.Current; - using (var commandFile = new SqlCommand("Delete from [File] where [Id]=@Id")) - using (var commandSimilar = new SqlCommand("Delete from [SimilarFile] where [File1Id]=@Id or [File2Id]=@Id")) + using (var commandFile = new SqliteCommand("Delete from [File] where [Id]=@Id")) + using (var commandSimilar = new SqliteCommand("Delete from [SimilarFile] where [File1Id]=@Id or [File2Id]=@Id")) using (var transation = connection.BeginTransaction()) { commandFile.Connection = connection; commandFile.CommandTimeout = 0; commandFile.Transaction = transation; - commandFile.Parameters.Add(new SqlParameter("@Id", System.Data.SqlDbType.UniqueIdentifier)); + commandFile.Parameters.Add(new SqliteParameter("@Id", SqliteType.Blob)); commandSimilar.Connection = connection; commandSimilar.CommandTimeout = 0; commandSimilar.Transaction = transation; - commandSimilar.Parameters.Add(new SqlParameter("@Id", System.Data.SqlDbType.UniqueIdentifier)); + commandSimilar.Parameters.Add(new SqliteParameter("@Id", SqliteType.Blob)); foreach(var operation in operations) { - commandFile.Parameters[0].Value = operation.Item1; - commandSimilar.Parameters[0].Value = operation.Item1; + commandFile.Parameters[0].Value = operation.Item1.ToByteArray(); + commandSimilar.Parameters[0].Value = operation.Item1.ToByteArray(); commandSimilar.ExecuteNonQuery(); if (commandFile.ExecuteNonQuery() == 0) @@ -213,11 +213,11 @@ internal static int Delete(Guid folderId, string pathStart) int result; var connection = DatabaseConnection.Current; - using (var commandCreateTable = new SqlCommand("Create Table #tempFileId ([Id] uniqueidentifier)")) - using (var commandSelect = new SqlCommand("insert into #tempFileId select [Id] from [File] where [FolderId]=@FolderId and ")) - using (var commandDeleteSimilar = new SqlCommand("Delete from [SimilarFile] where [File1Id] in (select [Id] from #tempFileId) or [File2Id] in (select [Id] from #tempFileId)")) - using (var commandDeleteFile = new SqlCommand("Delete from [File] where [Id] in (select [Id] from #tempFileId)")) - using (var commandDropTable = new SqlCommand("Drop Table #tempFileId")) + using (var commandCreateTable = new SqliteCommand("Create temp table tempFileId ([Id] BLOB)")) + using (var commandSelect = new SqliteCommand("insert into tempFileId select [Id] from [File] where [FolderId]=@FolderId and ")) + using (var commandDeleteSimilar = new SqliteCommand("Delete from [SimilarFile] where [File1Id] in (select [Id] from tempFileId) or [File2Id] in (select [Id] from tempFileId)")) + using (var commandDeleteFile = new SqliteCommand("Delete from [File] where [Id] in (select [Id] from tempFileId)")) + using (var commandDropTable = new SqliteCommand("Drop Table tempFileId")) using (var transation = connection.BeginTransaction()) { commandCreateTable.Connection = connection; @@ -228,16 +228,18 @@ internal static int Delete(Guid folderId, string pathStart) commandSelect.Connection = connection; commandSelect.CommandTimeout = 0; commandSelect.Transaction = transation; - commandSelect.Parameters.Add(new SqlParameter("@FolderId", System.Data.SqlDbType.UniqueIdentifier) { Value = folderId }); + commandSelect.Parameters.AddGuid("@FolderId", folderId); if (pathStart == "") { commandSelect.CommandText += "[Path] = ''"; } else { - commandSelect.CommandText += "([Path] = @Path or [Path] like @PathStart)"; - commandSelect.Parameters.Add(new SqlParameter("@Path", System.Data.SqlDbType.NVarChar, 256) { Value = pathStart }); - commandSelect.Parameters.Add(new SqlParameter("@PathStart", System.Data.SqlDbType.NVarChar, 769) { Value = SqlServerLikeValueBuilder.Escape(pathStart + DirectorySeparatorString.Value) + "%" }); + //The escape clause belongs to the LIKE, not the equality test. + commandSelect.CommandText += "([Path] = @Path or [Path] like @PathStart" + + SqliteLikeValueBuilder.EscapeClause + ")"; + commandSelect.Parameters.AddText("@Path", pathStart); + commandSelect.Parameters.AddText("@PathStart", SqliteLikeValueBuilder.Escape(pathStart + DirectorySeparatorString.Value) + "%"); } if (commandSelect.ExecuteNonQuery() != 0) @@ -253,7 +255,7 @@ internal static int Delete(Guid folderId, string pathStart) if (SimilarFile.LoadImageHelper.cachePath != null) { - using (var commandReadId = new SqlCommand("Select [Id] from #tempFileId")) + using (var commandReadId = new SqliteCommand("Select [Id] from tempFileId")) { commandReadId.Connection = connection; commandReadId.CommandTimeout = 0; @@ -261,7 +263,7 @@ internal static int Delete(Guid folderId, string pathStart) using (var reader = commandReadId.ExecuteReader(System.Data.CommandBehavior.SequentialAccess)) { while (reader.Read()) - SimilarFile.LoadImageHelper.RemoveCache((Guid)reader[0]); + SimilarFile.LoadImageHelper.RemoveCache(reader.GetGuid(0)); reader.Close(); } } @@ -285,11 +287,11 @@ internal static int Delete(Guid folderId, string pathStart) internal static void Delete(Guid folderId, IEnumerable> operations) { var connection = DatabaseConnection.Current; - using (var commandCreateTable = new SqlCommand("Create Table #tempFileId ([Id] uniqueidentifier)")) - using (var commandSelect = new SqlCommand("insert into #tempFileId select [Id] from [File] where [FolderId]=@FolderId and [Path] = @Path")) - using (var commandDeleteSimilar = new SqlCommand("Delete from [SimilarFile] where [File1Id] in (select [Id] from #tempFileId) or [File2Id] in (select [Id] from #tempFileId)")) - using (var commandDeleteFile = new SqlCommand("Delete from [File] where [Id] in (select [Id] from #tempFileId)")) - using (var commandDropTable = new SqlCommand("Drop Table #tempFileId")) + using (var commandCreateTable = new SqliteCommand("Create temp table tempFileId ([Id] BLOB)")) + using (var commandSelect = new SqliteCommand("insert into tempFileId select [Id] from [File] where [FolderId]=@FolderId and [Path] = @Path")) + using (var commandDeleteSimilar = new SqliteCommand("Delete from [SimilarFile] where [File1Id] in (select [Id] from tempFileId) or [File2Id] in (select [Id] from tempFileId)")) + using (var commandDeleteFile = new SqliteCommand("Delete from [File] where [Id] in (select [Id] from tempFileId)")) + using (var commandDropTable = new SqliteCommand("Drop Table tempFileId")) using (var transation = connection.BeginTransaction()) { commandCreateTable.Connection = connection; @@ -297,8 +299,8 @@ internal static void Delete(Guid folderId, IEnumerable(reader[3]), Sha1Hash = DBNullableReader.ConvertFromReferenceType(reader[4]), - FileSize = (int)reader[5], - FileStateCode = (int)reader[6], - ImageComparedThreshold = (float)reader[7] + FileSize = reader.GetInt32(5), + FileStateCode = reader.GetInt32(6), + ImageComparedThreshold = reader.GetFloat(7) }; WriteObject(line); } diff --git a/ImageStore/File/GetFileCmdlet.cs b/ImageStore/File/GetFileCmdlet.cs index 0778f45..687881c 100644 --- a/ImageStore/File/GetFileCmdlet.cs +++ b/ImageStore/File/GetFileCmdlet.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Management.Automation; using System.Text; @@ -25,24 +25,24 @@ protected override void ProcessRecord() internal static ImageStoreFile GetFile(Guid id) { var connection = DatabaseConnection.Current; - using (var command = new SqlCommand("Select [FolderId],[Path],[FileName],[ExtensionId],[ImageHash],[Sha1Hash],[FileSize],[FileState],[ImageComparedThreshold] from [File] Where [Id]=@Id")) + using (var command = new SqliteCommand("Select [FolderId],[Path],[FileName],[ExtensionId],[ImageHash],[Sha1Hash],[FileSize],[FileState],[ImageComparedThreshold] from [File] Where [Id]=@Id")) { command.Connection = connection; command.CommandTimeout = 0; - command.Parameters.Add(new SqlParameter("@Id", System.Data.SqlDbType.UniqueIdentifier) { Value = id }); + command.Parameters.AddGuid("@Id", id); using (var reader = command.ExecuteReader(System.Data.CommandBehavior.SequentialAccess)) { ImageStoreFile result; if (reader.Read()) { - result = new ImageStoreFile(id, (Guid)reader[0], (string)reader[1], (string)reader[2], (Guid)reader[3]) + result = new ImageStoreFile(id, reader.GetGuid(0), reader.GetString(1), reader.GetString(2), reader.GetGuid(3)) { ImageHash = DBNullableReader.ConvertFromReferenceType(reader[4]), Sha1Hash = DBNullableReader.ConvertFromReferenceType(reader[5]), - FileSize = (int)reader[6], - FileStateCode = (int)reader[7], - ImageComparedThreshold = (float)reader[8] + FileSize = reader.GetInt32(6), + FileStateCode = reader.GetInt32(7), + ImageComparedThreshold = reader.GetFloat(8) }; } else diff --git a/ImageStore/File/MeasureFileCmdlet.cs b/ImageStore/File/MeasureFileCmdlet.cs index b6c3c51..e32a6ed 100644 --- a/ImageStore/File/MeasureFileCmdlet.cs +++ b/ImageStore/File/MeasureFileCmdlet.cs @@ -3,7 +3,7 @@ using Shipwreck.Phash; using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Management.Automation; using System.Text; @@ -94,17 +94,17 @@ protected override void ProcessRecord() if (isRecomputing) { var connection = DatabaseConnection.Current; - using (var commandToDeleteSame = new SqlCommand("Delete from [SameFile] Where [FileId]=@Id")) - using (var commandToDeleteSimilar = new SqlCommand("Delete from [SimilarFile] Where [File1Id]=@Id or [File2Id]=@Id")) + using (var commandToDeleteSame = new SqliteCommand("Delete from [SameFile] Where [FileId]=@Id")) + using (var commandToDeleteSimilar = new SqliteCommand("Delete from [SimilarFile] Where [File1Id]=@Id or [File2Id]=@Id")) { commandToDeleteSame.Connection = connection; commandToDeleteSame.CommandTimeout = 0; - commandToDeleteSame.Parameters.Add(new SqlParameter("@Id", System.Data.SqlDbType.UniqueIdentifier) { Value = File.Id }); + commandToDeleteSame.Parameters.AddGuid("@Id", File.Id); commandToDeleteSame.ExecuteNonQuery(); commandToDeleteSimilar.Connection = connection; commandToDeleteSimilar.CommandTimeout = 0; - commandToDeleteSimilar.Parameters.Add(new SqlParameter("@Id", System.Data.SqlDbType.UniqueIdentifier) { Value = File.Id }); + commandToDeleteSimilar.Parameters.AddGuid("@Id", File.Id); commandToDeleteSimilar.ExecuteNonQuery(); } } diff --git a/ImageStore/File/MeasureFilesCmdlet.cs b/ImageStore/File/MeasureFilesCmdlet.cs index fba40f5..9c2534f 100644 --- a/ImageStore/File/MeasureFilesCmdlet.cs +++ b/ImageStore/File/MeasureFilesCmdlet.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Management.Automation; using System.Text; @@ -302,23 +302,23 @@ void WriteDatabase() var connection = DatabaseConnection.Current; - using (var commandToDeleteSame = new SqlCommand("Delete from [SameFile] Where [FileId]=@Id")) - using (var commandToDeleteSimilar = new SqlCommand("Delete from [SimilarFile] Where [File1Id]=@Id or [File2Id]=@Id")) - using (var command = new SqlCommand("Update [File] Set [ImageHash]=@ImageHash, [Sha1Hash]=@Sha1Hash, [FileSize]=@FileSize, [FileState]=@FileState, [ImageComparedThreshold]=0 where [Id]=@Id")) + using (var commandToDeleteSame = new SqliteCommand("Delete from [SameFile] Where [FileId]=@Id")) + using (var commandToDeleteSimilar = new SqliteCommand("Delete from [SimilarFile] Where [File1Id]=@Id or [File2Id]=@Id")) + using (var command = new SqliteCommand("Update [File] Set [ImageHash]=@ImageHash, [Sha1Hash]=@Sha1Hash, [FileSize]=@FileSize, [FileState]=@FileState, [ImageComparedThreshold]=0 where [Id]=@Id")) { commandToDeleteSame.Connection = connection; commandToDeleteSame.CommandTimeout = 0; - commandToDeleteSame.Parameters.Add(new SqlParameter("@Id", System.Data.SqlDbType.UniqueIdentifier)); + commandToDeleteSame.Parameters.Add(new SqliteParameter("@Id", SqliteType.Blob)); commandToDeleteSimilar.Connection = connection; commandToDeleteSimilar.CommandTimeout = 0; - commandToDeleteSimilar.Parameters.Add(new SqlParameter("@Id", System.Data.SqlDbType.UniqueIdentifier)); + commandToDeleteSimilar.Parameters.Add(new SqliteParameter("@Id", SqliteType.Blob)); command.Connection = connection; command.CommandTimeout = 0; - command.Parameters.Add(new SqlParameter("@Id", System.Data.SqlDbType.UniqueIdentifier)); - command.Parameters.Add(new SqlParameter("@ImageHash", System.Data.SqlDbType.Binary, 40)); - command.Parameters.Add(new SqlParameter("@Sha1Hash", System.Data.SqlDbType.Binary, 20)); - command.Parameters.Add(new SqlParameter("@FileSize", System.Data.SqlDbType.Int)); - command.Parameters.Add(new SqlParameter("@FileState", System.Data.SqlDbType.Int)); + command.Parameters.Add(new SqliteParameter("@Id", SqliteType.Blob)); + command.Parameters.Add(new SqliteParameter("@ImageHash", SqliteType.Blob)); + command.Parameters.Add(new SqliteParameter("@Sha1Hash", SqliteType.Blob)); + command.Parameters.Add(new SqliteParameter("@FileSize", SqliteType.Integer)); + command.Parameters.Add(new SqliteParameter("@FileState", SqliteType.Integer)); Tuple record; @@ -328,7 +328,7 @@ void WriteDatabase() { record = toWrite.Take(); - command.Parameters[0].Value = record.Item1; + command.Parameters[0].Value = record.Item1.ToByteArray(); command.Parameters[1].Value = DBNullableReader.NullCheck(record.Item2); command.Parameters[2].Value = DBNullableReader.NullCheck(record.Item3); command.Parameters[3].Value = record.Item4; @@ -343,9 +343,9 @@ record = toWrite.Take(); if (!record.Item6) { - commandToDeleteSame.Parameters[0].Value = record.Item1; + commandToDeleteSame.Parameters[0].Value = record.Item1.ToByteArray(); commandToDeleteSame.ExecuteNonQuery(); - commandToDeleteSimilar.Parameters[0].Value = record.Item1; + commandToDeleteSimilar.Parameters[0].Value = record.Item1.ToByteArray(); commandToDeleteSimilar.ExecuteNonQuery(); } } diff --git a/ImageStore/File/MoveFileCmdlet.cs b/ImageStore/File/MoveFileCmdlet.cs index 186509a..f525fb9 100644 --- a/ImageStore/File/MoveFileCmdlet.cs +++ b/ImageStore/File/MoveFileCmdlet.cs @@ -1,7 +1,7 @@ using SecretNest.ImageStore.Folder; using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Management.Automation; using System.Text; @@ -97,13 +97,13 @@ protected override void ProcessRecord() } } - using (var command = new SqlCommand("Update [File] Set [FolderId]=@FolderId, [Path]=@Path where [Id]=@Id")) + using (var command = new SqliteCommand("Update [File] Set [FolderId]=@FolderId, [Path]=@Path where [Id]=@Id")) { command.Connection = connection; command.CommandTimeout = 0; - command.Parameters.Add(new SqlParameter("@Id", System.Data.SqlDbType.UniqueIdentifier) { Value = Id }); - command.Parameters.Add(new SqlParameter("@FolderId", System.Data.SqlDbType.UniqueIdentifier) { Value = NewFolderId }); - command.Parameters.Add(new SqlParameter("@Path", System.Data.SqlDbType.NVarChar, 256) { Value = NewPath }); + command.Parameters.AddGuid("@Id", Id); + command.Parameters.AddGuid("@FolderId", NewFolderId); + command.Parameters.AddText("@Path", NewPath); if (command.ExecuteNonQuery() > 0) { diff --git a/ImageStore/File/RemoveDirectoryCmdlet.cs b/ImageStore/File/RemoveDirectoryCmdlet.cs index 3c03971..e875461 100644 --- a/ImageStore/File/RemoveDirectoryCmdlet.cs +++ b/ImageStore/File/RemoveDirectoryCmdlet.cs @@ -1,7 +1,7 @@ using SecretNest.ImageStore.Folder; using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.IO; using System.Linq; using System.Management.Automation; diff --git a/ImageStore/File/RemoveFileCmdlet.cs b/ImageStore/File/RemoveFileCmdlet.cs index 4eb3421..2e6726c 100644 --- a/ImageStore/File/RemoveFileCmdlet.cs +++ b/ImageStore/File/RemoveFileCmdlet.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Management.Automation; using System.Text; diff --git a/ImageStore/File/RenameFileCmdlet.cs b/ImageStore/File/RenameFileCmdlet.cs index 8a105c9..9eff69e 100644 --- a/ImageStore/File/RenameFileCmdlet.cs +++ b/ImageStore/File/RenameFileCmdlet.cs @@ -1,7 +1,7 @@ using SecretNest.ImageStore.Extension; using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Management.Automation; using System.Text; @@ -88,13 +88,13 @@ protected override void ProcessRecord() } } - using (var command = new SqlCommand("Update [File] Set [FileName]=@FileName, [ExtensionId]=@ExtensionId where [Id]=@Id")) + using (var command = new SqliteCommand("Update [File] Set [FileName]=@FileName, [ExtensionId]=@ExtensionId where [Id]=@Id")) { command.Connection = connection; command.CommandTimeout = 0; - command.Parameters.Add(new SqlParameter("@Id", System.Data.SqlDbType.UniqueIdentifier) { Value = Id }); - command.Parameters.Add(new SqlParameter("@FileName", System.Data.SqlDbType.NVarChar, 256) { Value = NewFileName }); - command.Parameters.Add(new SqlParameter("@ExtensionId", System.Data.SqlDbType.UniqueIdentifier) { Value = NewExtensionId }); + command.Parameters.AddGuid("@Id", Id); + command.Parameters.AddText("@FileName", NewFileName); + command.Parameters.AddGuid("@ExtensionId", NewExtensionId); if (command.ExecuteNonQuery() > 0) { diff --git a/ImageStore/File/SearchFileCmdlet.cs b/ImageStore/File/SearchFileCmdlet.cs index fdbab20..828f86c 100644 --- a/ImageStore/File/SearchFileCmdlet.cs +++ b/ImageStore/File/SearchFileCmdlet.cs @@ -1,7 +1,7 @@ using SecretNest.ImageStore.DatabaseShared; using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Management.Automation; using System.Text; @@ -85,18 +85,13 @@ protected override void ProcessRecord() { var connection = DatabaseConnection.Current; - using (var command = new SqlCommand(" [Id],[FolderId],[Path],[FileName],[ExtensionId],[ImageHash],[Sha1Hash],[FileSize],[FileState],[ImageComparedThreshold] from [File]")) + using (var command = new SqliteCommand(" [Id],[FolderId],[Path],[FileName],[ExtensionId],[ImageHash],[Sha1Hash],[FileSize],[FileState],[ImageComparedThreshold] from [File]")) { command.Connection = connection; command.CommandTimeout = 0; - if (Top.HasValue) - { - command.CommandText = "SELECT TOP " + Top.Value.ToString() + command.CommandText; - } - else - { - command.CommandText = "SELECT" + command.CommandText; - } + //SQLite has no TOP; the row limit goes on the end as LIMIT, after + //the where and order by clauses appended below. + command.CommandText = "SELECT" + command.CommandText; WhereCauseBuilder whereCauseBuilder = new WhereCauseBuilder(command.Parameters); whereCauseBuilder.AddUniqueIdentifierComparingCause("FolderId", FolderId); @@ -127,19 +122,22 @@ protected override void ProcessRecord() command.CommandText += " order by [FolderId], [Path], [FileName], [ExtensionId], [FileState]"; + if (Top.HasValue) + command.CommandText += " limit " + Top.Value.ToString(); + List result = new List(); using (var reader = command.ExecuteReader(System.Data.CommandBehavior.SequentialAccess)) { while (reader.Read()) { - ImageStoreFile line = new ImageStoreFile((Guid)reader[0], (Guid)reader[1], (string)reader[2], (string)reader[3], (Guid)reader[4]) + ImageStoreFile line = new ImageStoreFile(reader.GetGuid(0), reader.GetGuid(1), reader.GetString(2), reader.GetString(3), reader.GetGuid(4)) { ImageHash = DBNullableReader.ConvertFromReferenceType(reader[5]), Sha1Hash = DBNullableReader.ConvertFromReferenceType(reader[6]), - FileSize = (int)reader[7], - FileStateCode = (int)reader[8], - ImageComparedThreshold = (float)reader[9] + FileSize = reader.GetInt32(7), + FileStateCode = reader.GetInt32(8), + ImageComparedThreshold = reader.GetFloat(9) }; result.Add(line); } diff --git a/ImageStore/File/UpdateFileCmdlet.cs b/ImageStore/File/UpdateFileCmdlet.cs index d1a03ec..ab0563d 100644 --- a/ImageStore/File/UpdateFileCmdlet.cs +++ b/ImageStore/File/UpdateFileCmdlet.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Management.Automation; using System.Text; @@ -32,16 +32,16 @@ internal static int UpdateRecord(ImageStoreFile file) { var connection = DatabaseConnection.Current; - using (var command = new SqlCommand("Update [File] Set [ImageHash]=@ImageHash, [Sha1Hash]=@Sha1Hash, [FileSize]=@FileSize, [FileState]=@FileState, [ImageComparedThreshold]=@ImageComparedThreshold where [Id]=@Id")) + using (var command = new SqliteCommand("Update [File] Set [ImageHash]=@ImageHash, [Sha1Hash]=@Sha1Hash, [FileSize]=@FileSize, [FileState]=@FileState, [ImageComparedThreshold]=@ImageComparedThreshold where [Id]=@Id")) { command.Connection = connection; command.CommandTimeout = 0; - command.Parameters.Add(new SqlParameter("@Id", System.Data.SqlDbType.UniqueIdentifier) { Value = file.Id }); - command.Parameters.Add(new SqlParameter("@ImageHash", System.Data.SqlDbType.Binary, 40) { Value = DBNullableReader.NullCheck(file.ImageHash) }); - command.Parameters.Add(new SqlParameter("@Sha1Hash", System.Data.SqlDbType.Binary, 20) { Value = DBNullableReader.NullCheck(file.Sha1Hash) }); - command.Parameters.Add(new SqlParameter("@FileSize", System.Data.SqlDbType.Int) { Value = file.FileSize }); - command.Parameters.Add(new SqlParameter("@FileState", System.Data.SqlDbType.Int) { Value = file.FileStateCode }); - command.Parameters.Add(new SqlParameter("@ImageComparedThreshold", System.Data.SqlDbType.Real) { Value = file.ImageComparedThreshold }); + command.Parameters.AddGuid("@Id", file.Id); + command.Parameters.AddBlob("@ImageHash", file.ImageHash); + command.Parameters.AddBlob("@Sha1Hash", file.Sha1Hash); + command.Parameters.AddInt("@FileSize", file.FileSize); + command.Parameters.AddInt("@FileState", file.FileStateCode); + command.Parameters.AddReal("@ImageComparedThreshold", file.ImageComparedThreshold); return command.ExecuteNonQuery(); } diff --git a/ImageStore/Folder/AddFolderCmdlet.cs b/ImageStore/Folder/AddFolderCmdlet.cs index b6e1ab1..e1d2fab 100644 --- a/ImageStore/Folder/AddFolderCmdlet.cs +++ b/ImageStore/Folder/AddFolderCmdlet.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Management.Automation; using System.Text; @@ -37,15 +37,15 @@ protected override void ProcessRecord() var connection = DatabaseConnection.Current; var id = Guid.NewGuid(); - using (var command = new SqlCommand("Insert into [Folder] values(@Id, @Name, @Path, @CompareImageWith, @IsSealed)")) + using (var command = new SqliteCommand("Insert into [Folder] values(@Id, @Name, @Path, @CompareImageWith, @IsSealed)")) { command.Connection = connection; command.CommandTimeout = 0; - command.Parameters.Add(new SqlParameter("@Id", System.Data.SqlDbType.UniqueIdentifier) { Value = id }); - command.Parameters.Add(new SqlParameter("@Name", System.Data.SqlDbType.NVarChar, 256) { Value = Name }); - command.Parameters.Add(new SqlParameter("@Path", System.Data.SqlDbType.NVarChar, 256) { Value = Path }); - command.Parameters.Add(new SqlParameter("@CompareImageWith", System.Data.SqlDbType.Int) { Value = (int)CompareImageWith }); - command.Parameters.Add(new SqlParameter("@IsSealed", System.Data.SqlDbType.Bit) { Value = IsSealed }); + command.Parameters.AddGuid("@Id", id); + command.Parameters.AddText("@Name", Name); + command.Parameters.AddText("@Path", Path); + command.Parameters.AddInt("@CompareImageWith", (int)CompareImageWith); + command.Parameters.AddBool("@IsSealed", IsSealed); if (command.ExecuteNonQuery() > 0) { diff --git a/ImageStore/Folder/FindFolderCmdlet.cs b/ImageStore/Folder/FindFolderCmdlet.cs index 3b1f965..bc7e843 100644 --- a/ImageStore/Folder/FindFolderCmdlet.cs +++ b/ImageStore/Folder/FindFolderCmdlet.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Management.Automation; using System.Text; @@ -23,21 +23,21 @@ protected override void ProcessRecord() throw new ArgumentNullException(nameof(Name)); var connection = DatabaseConnection.Current; - using (var command = new SqlCommand("Select [Id],[Path],[Name],[CompareImageWith],[IsSealed] from [Folder] Where [Name]=@Name")) + using (var command = new SqliteCommand("Select [Id],[Path],[Name],[CompareImageWith],[IsSealed] from [Folder] Where [Name]=@Name")) { command.Connection = connection; command.CommandTimeout = 0; - command.Parameters.Add(new SqlParameter("@Name", System.Data.SqlDbType.NVarChar, 256) { Value = Name }); + command.Parameters.AddText("@Name", Name); using (var reader = command.ExecuteReader(System.Data.CommandBehavior.SequentialAccess)) { if (reader.Read()) { - ImageStoreFolder line = new ImageStoreFolder((Guid)reader[0],(string)reader[1]) + ImageStoreFolder line = new ImageStoreFolder(reader.GetGuid(0),reader.GetString(1)) { - Name = (string)reader[2], - CompareImageWithCode = (int)reader[3], - IsSealed = (bool)reader[4] + Name = reader.GetString(2), + CompareImageWithCode = reader.GetInt32(3), + IsSealed = reader.GetBoolean(4) }; WriteObject(line); } diff --git a/ImageStore/Folder/FolderHelper.cs b/ImageStore/Folder/FolderHelper.cs index deb9637..c405d55 100644 --- a/ImageStore/Folder/FolderHelper.cs +++ b/ImageStore/Folder/FolderHelper.cs @@ -1,7 +1,7 @@ using SecretNest.ImageStore.Folder; using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -14,7 +14,7 @@ internal static IEnumerable GetAllFolders() { var connection = DatabaseConnection.Current; - using (var command = new SqlCommand("Select [Id],[Path],[Name],[CompareImageWith],[IsSealed] from [Folder]")) + using (var command = new SqliteCommand("Select [Id],[Path],[Name],[CompareImageWith],[IsSealed] from [Folder]")) { command.Connection = connection; command.CommandTimeout = 0; @@ -23,11 +23,11 @@ internal static IEnumerable GetAllFolders() { while (reader.Read()) { - ImageStoreFolder line = new ImageStoreFolder((Guid)reader[0], (string)reader[1]) + ImageStoreFolder line = new ImageStoreFolder(reader.GetGuid(0), reader.GetString(1)) { - Name = (string)reader[2], - CompareImageWithCode = (int)reader[3], - IsSealed = (bool)reader[4] + Name = reader.GetString(2), + CompareImageWithCode = reader.GetInt32(3), + IsSealed = reader.GetBoolean(4) }; yield return line; } @@ -40,19 +40,19 @@ internal static IEnumerable GetAllFolders() internal static string GetFolderPath(Guid id, out bool isSealed) { var connection = DatabaseConnection.Current; - using (var command = new SqlCommand("Select [Path],[IsSealed] from [Folder] Where [Id]=@Id")) + using (var command = new SqliteCommand("Select [Path],[IsSealed] from [Folder] Where [Id]=@Id")) { command.Connection = connection; command.CommandTimeout = 0; - command.Parameters.Add(new SqlParameter("@Id", System.Data.SqlDbType.UniqueIdentifier) { Value = id }); + command.Parameters.AddGuid("@Id", id); using (var reader = command.ExecuteReader(System.Data.CommandBehavior.SequentialAccess)) { string result; if (reader.Read()) { - result = (string)reader[0]; - isSealed = (bool)reader[1]; + result = reader.GetString(0); + isSealed = reader.GetBoolean(1); } else { diff --git a/ImageStore/Folder/GetFolderCmdlet.cs b/ImageStore/Folder/GetFolderCmdlet.cs index ae32628..1288efc 100644 --- a/ImageStore/Folder/GetFolderCmdlet.cs +++ b/ImageStore/Folder/GetFolderCmdlet.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Management.Automation; using System.Text; @@ -19,21 +19,21 @@ public class GetFolderCmdlet : Cmdlet protected override void ProcessRecord() { var connection = DatabaseConnection.Current; - using (var command = new SqlCommand("Select [Path],[Name],[CompareImageWith],[IsSealed] from [Folder] Where [Id]=@Id")) + using (var command = new SqliteCommand("Select [Path],[Name],[CompareImageWith],[IsSealed] from [Folder] Where [Id]=@Id")) { command.Connection = connection; command.CommandTimeout = 0; - command.Parameters.Add(new SqlParameter("@Id", System.Data.SqlDbType.UniqueIdentifier) { Value = Id }); + command.Parameters.AddGuid("@Id", Id); using (var reader = command.ExecuteReader(System.Data.CommandBehavior.SequentialAccess)) { if (reader.Read()) { - ImageStoreFolder line = new ImageStoreFolder(Id,(string)reader[0]) + ImageStoreFolder line = new ImageStoreFolder(Id,reader.GetString(0)) { - Name = (string)reader[1], - CompareImageWithCode = (int)reader[2], - IsSealed = (bool)reader[3] + Name = reader.GetString(1), + CompareImageWithCode = reader.GetInt32(2), + IsSealed = reader.GetBoolean(3) }; WriteObject(line); } diff --git a/ImageStore/Folder/RemoveFolderCmdlet.cs b/ImageStore/Folder/RemoveFolderCmdlet.cs index 0cd0193..0081fd5 100644 --- a/ImageStore/Folder/RemoveFolderCmdlet.cs +++ b/ImageStore/Folder/RemoveFolderCmdlet.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Management.Automation; using System.Management.Automation.Runspaces; @@ -26,11 +26,11 @@ protected override void ProcessRecord() var connection = DatabaseConnection.Current; - using (var commandCreateTable = new SqlCommand("Create Table #tempFileId ([Id] uniqueidentifier)")) - using (var commandSelect = new SqlCommand("insert into #tempFileId select [Id] from [File] where [FolderId]=@FolderId")) - using (var commandDeleteSimilar = new SqlCommand("Delete from [SimilarFile] where [File1Id] in (select [Id] from #tempFileId) or [File2Id] in (select [Id] from #tempFileId)")) - using (var commandDropTable = new SqlCommand("Drop Table #tempFileId")) - using (var commandDeleteFolder= new SqlCommand("Delete from [Folder] where [Id]=@Id")) + using (var commandCreateTable = new SqliteCommand("Create temp table tempFileId ([Id] BLOB)")) + using (var commandSelect = new SqliteCommand("insert into tempFileId select [Id] from [File] where [FolderId]=@FolderId")) + using (var commandDeleteSimilar = new SqliteCommand("Delete from [SimilarFile] where [File1Id] in (select [Id] from tempFileId) or [File2Id] in (select [Id] from tempFileId)")) + using (var commandDropTable = new SqliteCommand("Drop Table tempFileId")) + using (var commandDeleteFolder= new SqliteCommand("Delete from [Folder] where [Id]=@Id")) using (var transation = connection.BeginTransaction()) { commandCreateTable.Connection = connection; @@ -41,7 +41,7 @@ protected override void ProcessRecord() commandSelect.Connection = connection; commandSelect.CommandTimeout = 0; commandSelect.Transaction = transation; - commandSelect.Parameters.Add(new SqlParameter("@FolderId", System.Data.SqlDbType.UniqueIdentifier) { Value = Id }); + commandSelect.Parameters.AddGuid("@FolderId", Id); if (commandSelect.ExecuteNonQuery() != 0) { @@ -52,7 +52,7 @@ protected override void ProcessRecord() if (SimilarFile.LoadImageHelper.cachePath != null) { - using (var commandReadId = new SqlCommand("Select [Id] from #tempFileId")) + using (var commandReadId = new SqliteCommand("Select [Id] from tempFileId")) { commandReadId.Connection = connection; commandReadId.CommandTimeout = 0; @@ -60,7 +60,7 @@ protected override void ProcessRecord() using (var reader = commandReadId.ExecuteReader(System.Data.CommandBehavior.SequentialAccess)) { while (reader.Read()) - SimilarFile.LoadImageHelper.RemoveCache((Guid)reader[0]); + SimilarFile.LoadImageHelper.RemoveCache(reader.GetGuid(0)); reader.Close(); } } @@ -74,7 +74,7 @@ protected override void ProcessRecord() commandDeleteFolder.Connection = connection; commandDeleteFolder.CommandTimeout = 0; commandDeleteFolder.Transaction = transation; - commandDeleteFolder.Parameters.Add(new SqlParameter("@Id", System.Data.SqlDbType.UniqueIdentifier) { Value = Id }); + commandDeleteFolder.Parameters.AddGuid("@Id", Id); int result = commandDeleteFolder.ExecuteNonQuery(); if (result == 0) diff --git a/ImageStore/Folder/SearchFolderCmdlet.cs b/ImageStore/Folder/SearchFolderCmdlet.cs index b24d616..328c8d2 100644 --- a/ImageStore/Folder/SearchFolderCmdlet.cs +++ b/ImageStore/Folder/SearchFolderCmdlet.cs @@ -1,7 +1,7 @@ using SecretNest.ImageStore.DatabaseShared; using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Management.Automation; using System.Text; @@ -36,7 +36,7 @@ protected override void ProcessRecord() { var connection = DatabaseConnection.Current; - using (var command = new SqlCommand("Select [Id],[Path],[Name],[CompareImageWith],[IsSealed] from [Folder]")) + using (var command = new SqliteCommand("Select [Id],[Path],[Name],[CompareImageWith],[IsSealed] from [Folder]")) { command.Connection = connection; command.CommandTimeout = 0; @@ -55,11 +55,11 @@ protected override void ProcessRecord() { while (reader.Read()) { - ImageStoreFolder line = new ImageStoreFolder((Guid)reader[0],(string)reader[1]) + ImageStoreFolder line = new ImageStoreFolder(reader.GetGuid(0),reader.GetString(1)) { - Name = (string)reader[2], - CompareImageWithCode = (int)reader[3], - IsSealed = (bool)reader[4] + Name = reader.GetString(2), + CompareImageWithCode = reader.GetInt32(3), + IsSealed = reader.GetBoolean(4) }; result.Add(line); } diff --git a/ImageStore/Folder/SyncFolderCmdlet.cs b/ImageStore/Folder/SyncFolderCmdlet.cs index 4e1fecf..962f336 100644 --- a/ImageStore/Folder/SyncFolderCmdlet.cs +++ b/ImageStore/Folder/SyncFolderCmdlet.cs @@ -4,7 +4,7 @@ using SecretNest.ImageStore.IgnoredDirectory; using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.IO; using System.Linq; using System.Management.Automation; @@ -193,15 +193,15 @@ void ProcessDirectory(ImageStoreFolder folder, string directoryName, string orig if (allFiles.Length != 0) { - using (var command = new SqlCommand("Insert into [File] values(@Id, @FolderId, @Path, @FileName, @ExtensionId, NULL, NULL, -1, 0, 0)")) + using (var command = new SqliteCommand("Insert into [File] values(@Id, @FolderId, @Path, @FileName, @ExtensionId, NULL, NULL, -1, 0, 0)")) { command.Connection = connection; command.CommandTimeout = 0; - command.Parameters.Add(new SqlParameter("@Id", System.Data.SqlDbType.UniqueIdentifier)); - command.Parameters.Add(new SqlParameter("@FolderId", System.Data.SqlDbType.UniqueIdentifier) { Value = folder.Id }); - command.Parameters.Add(new SqlParameter("@Path", System.Data.SqlDbType.NVarChar, 256)); - command.Parameters.Add(new SqlParameter("@FileName", System.Data.SqlDbType.NVarChar, 256)); - command.Parameters.Add(new SqlParameter("@ExtensionId", System.Data.SqlDbType.UniqueIdentifier)); + command.Parameters.Add(new SqliteParameter("@Id", SqliteType.Blob)); + command.Parameters.AddGuid("@FolderId", folder.Id); + command.Parameters.Add(new SqliteParameter("@Path", SqliteType.Text)); + command.Parameters.Add(new SqliteParameter("@FileName", SqliteType.Text)); + command.Parameters.Add(new SqliteParameter("@ExtensionId", SqliteType.Blob)); foreach (var fullFilePath in allFiles) { @@ -235,10 +235,10 @@ void ProcessDirectory(ImageStoreFolder folder, string directoryName, string orig if (dbFilesInDirectory == null || !dbFilesInDirectory.Remove(fileNameKey)) { //Add file - command.Parameters[0].Value = Guid.NewGuid(); + command.Parameters[0].Value = Guid.NewGuid().ToByteArray(); command.Parameters[2].Value = directoryName; command.Parameters[3].Value = fileNameWithoutExtension; - command.Parameters[4].Value = extension.Id; + command.Parameters[4].Value = extension.Id.ToByteArray(); if (command.ExecuteNonQuery() == 0) { diff --git a/ImageStore/Folder/UpdateFolderCmdlet.cs b/ImageStore/Folder/UpdateFolderCmdlet.cs index 85400b8..846b33f 100644 --- a/ImageStore/Folder/UpdateFolderCmdlet.cs +++ b/ImageStore/Folder/UpdateFolderCmdlet.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Management.Automation; using System.Text; @@ -22,15 +22,15 @@ protected override void ProcessRecord() var connection = DatabaseConnection.Current; - using (var command = new SqlCommand("Update [Folder] Set [Name]=@Name, [Path]=@Path, [CompareImageWith]=@CompareImageWith, [IsSealed]=@IsSealed where [Id]=@Id")) + using (var command = new SqliteCommand("Update [Folder] Set [Name]=@Name, [Path]=@Path, [CompareImageWith]=@CompareImageWith, [IsSealed]=@IsSealed where [Id]=@Id")) { command.Connection = connection; command.CommandTimeout = 0; - command.Parameters.Add(new SqlParameter("@Id", System.Data.SqlDbType.UniqueIdentifier) { Value = Folder.Id }); - command.Parameters.Add(new SqlParameter("@Name", System.Data.SqlDbType.NVarChar, 256) { Value = Folder.Name }); - command.Parameters.Add(new SqlParameter("@Path", System.Data.SqlDbType.NVarChar, 256) { Value = Folder.Path }); - command.Parameters.Add(new SqlParameter("@CompareImageWith", System.Data.SqlDbType.Int) { Value = Folder.CompareImageWithCode }); - command.Parameters.Add(new SqlParameter("@IsSealed", System.Data.SqlDbType.Bit) { Value = Folder.IsSealed }); + command.Parameters.AddGuid("@Id", Folder.Id); + command.Parameters.AddText("@Name", Folder.Name); + command.Parameters.AddText("@Path", Folder.Path); + command.Parameters.AddInt("@CompareImageWith", Folder.CompareImageWithCode); + command.Parameters.AddBool("@IsSealed", Folder.IsSealed); if (command.ExecuteNonQuery() == 0) diff --git a/ImageStore/IgnoredDirectory/AddIgnoredDirectoryCmdlet.cs b/ImageStore/IgnoredDirectory/AddIgnoredDirectoryCmdlet.cs index c4f4d25..48e36e6 100644 --- a/ImageStore/IgnoredDirectory/AddIgnoredDirectoryCmdlet.cs +++ b/ImageStore/IgnoredDirectory/AddIgnoredDirectoryCmdlet.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Management.Automation; using System.Text; @@ -30,14 +30,14 @@ protected override void ProcessRecord() var connection = DatabaseConnection.Current; var id = Guid.NewGuid(); - using (var command = new SqlCommand("Insert into [IgnoredDirectory] values(@Id, @FolderId, @Directory, @IsSubDirectoryIncluded)")) + using (var command = new SqliteCommand("Insert into [IgnoredDirectory] values(@Id, @FolderId, @Directory, @IsSubDirectoryIncluded)")) { command.Connection = connection; command.CommandTimeout = 0; - command.Parameters.Add(new SqlParameter("@Id", System.Data.SqlDbType.UniqueIdentifier) { Value = id }); - command.Parameters.Add(new SqlParameter("@FolderId", System.Data.SqlDbType.UniqueIdentifier) { Value = FolderId }); - command.Parameters.Add(new SqlParameter("@Directory", System.Data.SqlDbType.NVarChar, 256) { Value = Directory }); - command.Parameters.Add(new SqlParameter("@IsSubDirectoryIncluded", System.Data.SqlDbType.Bit) { Value = IsSubDirectoryIncluded }); + command.Parameters.AddGuid("@Id", id); + command.Parameters.AddGuid("@FolderId", FolderId); + command.Parameters.AddText("@Directory", Directory); + command.Parameters.AddBool("@IsSubDirectoryIncluded", IsSubDirectoryIncluded); if (command.ExecuteNonQuery() > 0) { diff --git a/ImageStore/IgnoredDirectory/FindIgnoredDirectoryCmdlet.cs b/ImageStore/IgnoredDirectory/FindIgnoredDirectoryCmdlet.cs index ed6784c..3d1367b 100644 --- a/ImageStore/IgnoredDirectory/FindIgnoredDirectoryCmdlet.cs +++ b/ImageStore/IgnoredDirectory/FindIgnoredDirectoryCmdlet.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Management.Automation; using System.Text; @@ -28,22 +28,22 @@ protected override void ProcessRecord() throw new ArgumentNullException(nameof(Directory)); var connection = DatabaseConnection.Current; - using (var command = new SqlCommand("Select [Id],[Directory] from [IgnoredDirectory] Where [FolderId]=@FolderId and [Directory]=@Directory and [IsSubDirectoryIncluded]=@IsSubDirectoryIncluded")) + using (var command = new SqliteCommand("Select [Id],[Directory] from [IgnoredDirectory] Where [FolderId]=@FolderId and [Directory]=@Directory and [IsSubDirectoryIncluded]=@IsSubDirectoryIncluded")) { command.Connection = connection; command.CommandTimeout = 0; - command.Parameters.Add(new SqlParameter("@FolderId", System.Data.SqlDbType.UniqueIdentifier) { Value = FolderId }); - command.Parameters.Add(new SqlParameter("@Directory", System.Data.SqlDbType.NVarChar, 256) { Value = Directory }); - command.Parameters.Add(new SqlParameter("@IsSubDirectoryIncluded", System.Data.SqlDbType.Bit) { Value = IsSubDirectoryIncluded }); + command.Parameters.AddGuid("@FolderId", FolderId); + command.Parameters.AddText("@Directory", Directory); + command.Parameters.AddBool("@IsSubDirectoryIncluded", IsSubDirectoryIncluded); using (var reader = command.ExecuteReader(System.Data.CommandBehavior.SequentialAccess)) { if (reader.Read()) { - ImageStoreIgnoredDirectory line = new ImageStoreIgnoredDirectory((Guid)reader[0]) + ImageStoreIgnoredDirectory line = new ImageStoreIgnoredDirectory(reader.GetGuid(0)) { FolderId = FolderId, - Directory = (string)reader[1], + Directory = reader.GetString(1), IsSubDirectoryIncluded = IsSubDirectoryIncluded }; WriteObject(line); diff --git a/ImageStore/IgnoredDirectory/GetIgnoredDirectoryCmdlet.cs b/ImageStore/IgnoredDirectory/GetIgnoredDirectoryCmdlet.cs index 9cf85ee..f9d166b 100644 --- a/ImageStore/IgnoredDirectory/GetIgnoredDirectoryCmdlet.cs +++ b/ImageStore/IgnoredDirectory/GetIgnoredDirectoryCmdlet.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Management.Automation; using System.Text; @@ -19,11 +19,11 @@ public class GetIgnoredDirectoryCmdlet : Cmdlet protected override void ProcessRecord() { var connection = DatabaseConnection.Current; - using (var command = new SqlCommand("Select [FolderId],[Directory],[IsSubDirectoryIncluded] from [IgnoredDirectory] Where [Id]=@Id")) + using (var command = new SqliteCommand("Select [FolderId],[Directory],[IsSubDirectoryIncluded] from [IgnoredDirectory] Where [Id]=@Id")) { command.Connection = connection; command.CommandTimeout = 0; - command.Parameters.Add(new SqlParameter("@Id", System.Data.SqlDbType.UniqueIdentifier) { Value = Id }); + command.Parameters.AddGuid("@Id", Id); using (var reader = command.ExecuteReader(System.Data.CommandBehavior.SequentialAccess)) { @@ -31,9 +31,9 @@ protected override void ProcessRecord() { ImageStoreIgnoredDirectory line = new ImageStoreIgnoredDirectory(Id) { - FolderId = (Guid)reader[0], - Directory = (string)reader[1], - IsSubDirectoryIncluded = (bool)reader[2] + FolderId = reader.GetGuid(0), + Directory = reader.GetString(1), + IsSubDirectoryIncluded = reader.GetBoolean(2) }; WriteObject(line); } diff --git a/ImageStore/IgnoredDirectory/IgnoredDirectoryHelper.cs b/ImageStore/IgnoredDirectory/IgnoredDirectoryHelper.cs index dd222c6..3a53fb0 100644 --- a/ImageStore/IgnoredDirectory/IgnoredDirectoryHelper.cs +++ b/ImageStore/IgnoredDirectory/IgnoredDirectoryHelper.cs @@ -1,7 +1,7 @@ using SecretNest.ImageStore.IgnoredDirectory; using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -13,21 +13,21 @@ static class IgnoredDirectoryHelper internal static IEnumerable GetAllIgnoredDirectories(Guid folderId) { var connection = DatabaseConnection.Current; - using (var command = new SqlCommand("Select [Id],[Directory],[IsSubDirectoryIncluded] from [IgnoredDirectory] Where [FolderId]=@FolderId")) + using (var command = new SqliteCommand("Select [Id],[Directory],[IsSubDirectoryIncluded] from [IgnoredDirectory] Where [FolderId]=@FolderId")) { command.Connection = connection; command.CommandTimeout = 0; - command.Parameters.Add(new SqlParameter("@FolderId", System.Data.SqlDbType.UniqueIdentifier) { Value = folderId }); + command.Parameters.AddGuid("@FolderId", folderId); using (var reader = command.ExecuteReader(System.Data.CommandBehavior.SequentialAccess)) { while (reader.Read()) { - ImageStoreIgnoredDirectory line = new ImageStoreIgnoredDirectory((Guid)reader[0]) + ImageStoreIgnoredDirectory line = new ImageStoreIgnoredDirectory(reader.GetGuid(0)) { FolderId = folderId, - Directory = (string)reader[1], - IsSubDirectoryIncluded = (bool)reader[2] + Directory = reader.GetString(1), + IsSubDirectoryIncluded = reader.GetBoolean(2) }; yield return line; } diff --git a/ImageStore/IgnoredDirectory/RemoveIgnoredDirectoryCmdlet.cs b/ImageStore/IgnoredDirectory/RemoveIgnoredDirectoryCmdlet.cs index 2dc6e77..2a4f2ed 100644 --- a/ImageStore/IgnoredDirectory/RemoveIgnoredDirectoryCmdlet.cs +++ b/ImageStore/IgnoredDirectory/RemoveIgnoredDirectoryCmdlet.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Management.Automation; using System.Text; @@ -25,11 +25,11 @@ protected override void ProcessRecord() var connection = DatabaseConnection.Current; - using (var command = new SqlCommand("Delete from [IgnoredDirectory] where [Id]=@Id")) + using (var command = new SqliteCommand("Delete from [IgnoredDirectory] where [Id]=@Id")) { command.Connection = connection; command.CommandTimeout = 0; - command.Parameters.Add(new SqlParameter("@Id", System.Data.SqlDbType.UniqueIdentifier) { Value = Id }); + command.Parameters.AddGuid("@Id", Id); if (command.ExecuteNonQuery() == 0) { diff --git a/ImageStore/IgnoredDirectory/SearchIgnoredDirectoryCmdlet.cs b/ImageStore/IgnoredDirectory/SearchIgnoredDirectoryCmdlet.cs index 2d517a3..b0b542c 100644 --- a/ImageStore/IgnoredDirectory/SearchIgnoredDirectoryCmdlet.cs +++ b/ImageStore/IgnoredDirectory/SearchIgnoredDirectoryCmdlet.cs @@ -1,7 +1,7 @@ using SecretNest.ImageStore.DatabaseShared; using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Management.Automation; using System.Text; @@ -31,7 +31,7 @@ protected override void ProcessRecord() { var connection = DatabaseConnection.Current; - using (var command = new SqlCommand("Select [Id],[FolderId],[Directory],[IsSubDirectoryIncluded] from [IgnoredDirectory]")) + using (var command = new SqliteCommand("Select [Id],[FolderId],[Directory],[IsSubDirectoryIncluded] from [IgnoredDirectory]")) { command.Connection = connection; command.CommandTimeout = 0; @@ -49,11 +49,11 @@ protected override void ProcessRecord() { while (reader.Read()) { - ImageStoreIgnoredDirectory line = new ImageStoreIgnoredDirectory((Guid)reader[0]) + ImageStoreIgnoredDirectory line = new ImageStoreIgnoredDirectory(reader.GetGuid(0)) { - FolderId = (Guid)reader[1], - Directory = (string)reader[2], - IsSubDirectoryIncluded = (bool)reader[3] + FolderId = reader.GetGuid(1), + Directory = reader.GetString(2), + IsSubDirectoryIncluded = reader.GetBoolean(3) }; result.Add(line); } diff --git a/ImageStore/IgnoredDirectory/UpdateIgnoredDirectoryCmdlet.cs b/ImageStore/IgnoredDirectory/UpdateIgnoredDirectoryCmdlet.cs index bacc2ba..fb5944b 100644 --- a/ImageStore/IgnoredDirectory/UpdateIgnoredDirectoryCmdlet.cs +++ b/ImageStore/IgnoredDirectory/UpdateIgnoredDirectoryCmdlet.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; using System.Linq; using System.Management.Automation; using System.Text; @@ -22,14 +22,14 @@ protected override void ProcessRecord() var connection = DatabaseConnection.Current; - using (var command = new SqlCommand("Update [IgnoredDirectory] Set [FolderId]=@FolderId, [Directory]=@Directory, [IsSubDirectoryIncluded]=@IsSubDirectoryIncluded where [Id]=@Id")) + using (var command = new SqliteCommand("Update [IgnoredDirectory] Set [FolderId]=@FolderId, [Directory]=@Directory, [IsSubDirectoryIncluded]=@IsSubDirectoryIncluded where [Id]=@Id")) { command.Connection = connection; command.CommandTimeout = 0; - command.Parameters.Add(new SqlParameter("@Id", System.Data.SqlDbType.UniqueIdentifier) { Value = IgnoredDirectory.Id }); - command.Parameters.Add(new SqlParameter("@FolderId", System.Data.SqlDbType.UniqueIdentifier) { Value = IgnoredDirectory.FolderId }); - command.Parameters.Add(new SqlParameter("@Directory", System.Data.SqlDbType.NVarChar, 256) { Value = IgnoredDirectory.Directory }); - command.Parameters.Add(new SqlParameter("@IsSubDirectoryIncluded", System.Data.SqlDbType.Bit) { Value = IgnoredDirectory.IsSubDirectoryIncluded }); + command.Parameters.AddGuid("@Id", IgnoredDirectory.Id); + command.Parameters.AddGuid("@FolderId", IgnoredDirectory.FolderId); + command.Parameters.AddText("@Directory", IgnoredDirectory.Directory); + command.Parameters.AddBool("@IsSubDirectoryIncluded", IgnoredDirectory.IsSubDirectoryIncluded); if (command.ExecuteNonQuery() == 0) { diff --git a/ImageStore/ImageStore.csproj b/ImageStore/ImageStore.csproj index e5e5eec..3e74947 100644 --- a/ImageStore/ImageStore.csproj +++ b/ImageStore/ImageStore.csproj @@ -31,7 +31,7 @@ - +