strata-storage 2.5.0 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI-INTEGRATION-GUIDE.md +12 -3
- package/README.md +31 -9
- package/android/AGENTS.md +24 -7
- package/android/CLAUDE.md +42 -4
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/strata/storage/FilesystemStorage.java +287 -0
- package/android/src/main/java/com/strata/storage/SQLiteStorage.java +243 -221
- package/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +202 -78
- package/dist/README.md +31 -9
- package/dist/android/AGENTS.md +24 -7
- package/dist/android/CLAUDE.md +42 -4
- package/dist/android/build.gradle +1 -1
- package/dist/android/src/main/java/com/strata/storage/FilesystemStorage.java +287 -0
- package/dist/android/src/main/java/com/strata/storage/SQLiteStorage.java +243 -221
- package/dist/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +202 -78
- package/dist/ios/AGENTS.md +19 -4
- package/dist/ios/CLAUDE.md +39 -4
- package/dist/ios/Plugin/FilesystemStorage.swift +218 -0
- package/dist/ios/Plugin/SQLiteStorage.swift +265 -173
- package/dist/ios/Plugin/StrataStoragePlugin.swift +100 -42
- package/dist/package.json +1 -1
- package/ios/AGENTS.md +19 -4
- package/ios/CLAUDE.md +39 -4
- package/ios/Plugin/FilesystemStorage.swift +218 -0
- package/ios/Plugin/SQLiteStorage.swift +265 -173
- package/ios/Plugin/StrataStoragePlugin.swift +100 -42
- package/package.json +6 -6
package/AI-INTEGRATION-GUIDE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AI Integration Guide - strata-storage
|
|
2
2
|
|
|
3
|
-
Quick reference for AI development agents (Claude Code, Cursor, Copilot, etc.) to integrate `strata-storage` into web and mobile projects. Current version: **2.
|
|
3
|
+
Quick reference for AI development agents (Claude Code, Cursor, Copilot, etc.) to integrate `strata-storage` into web and mobile projects. Current version: **2.6.1**.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -160,15 +160,24 @@ const storage = defineStorage();
|
|
|
160
160
|
|
|
161
161
|
```typescript
|
|
162
162
|
import { defineStorage } from 'strata-storage';
|
|
163
|
-
import {
|
|
163
|
+
import {
|
|
164
|
+
PreferencesAdapter,
|
|
165
|
+
SecureStorageAdapter,
|
|
166
|
+
SqliteAdapter,
|
|
167
|
+
FilesystemAdapter,
|
|
168
|
+
} from 'strata-storage/capacitor';
|
|
164
169
|
|
|
165
170
|
const storage = defineStorage();
|
|
166
171
|
storage.registerAdapter(new PreferencesAdapter());
|
|
167
172
|
storage.registerAdapter(new SecureStorageAdapter());
|
|
173
|
+
storage.registerAdapter(new SqliteAdapter()); // 2.6.0 — multi-store (database + table)
|
|
174
|
+
storage.registerAdapter(new FilesystemAdapter()); // 2.6.0 — file-per-key, atomic writes
|
|
168
175
|
await storage.set('secret', token, { storage: 'secure' });
|
|
169
176
|
```
|
|
170
177
|
|
|
171
|
-
|
|
178
|
+
**SQLite multi-store** (2.6.0): each `new SqliteAdapter({ database, table })` is an isolated store — distinct `(database, table)` pairs map to distinct physical `.db` files / tables natively and cannot collide. **Filesystem** (2.6.0): one JSON file per key under the app's documents/files directory; `isAvailable()` now returns `true` on iOS and Android.
|
|
179
|
+
|
|
180
|
+
Native adapters depend on the downstream Capacitor project setup; follow `docs/guides/platforms/device-verification.md` to verify on a real iOS + Android device.
|
|
172
181
|
|
|
173
182
|
## Strata API (most-used methods)
|
|
174
183
|
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[](https://www.typescriptlang.org/)
|
|
10
10
|
[](https://github.com/aoneahsan/strata-storage)
|
|
11
11
|
|
|
12
|
-
- **Version:** `2.
|
|
12
|
+
- **Version:** `2.6.1`
|
|
13
13
|
- **License:** Apache-2.0
|
|
14
14
|
- **Node.js:** `>= 24.13.0`
|
|
15
15
|
- **Module format:** ESM only
|
|
@@ -390,15 +390,22 @@ const active = await storage.query({
|
|
|
390
390
|
|
|
391
391
|
### iOS and Android (via Capacitor)
|
|
392
392
|
|
|
393
|
-
Register native adapters
|
|
393
|
+
Register the native adapters you need when running under Capacitor. All four are zero-runtime-dependency: SQLite is hand-rolled (no plugin dependency) and filesystem uses the platform's native `FileManager` / `java.io.File`.
|
|
394
394
|
|
|
395
395
|
```typescript
|
|
396
396
|
import { defineStorage } from 'strata-storage';
|
|
397
|
-
import {
|
|
397
|
+
import {
|
|
398
|
+
PreferencesAdapter,
|
|
399
|
+
SecureStorageAdapter,
|
|
400
|
+
SqliteAdapter,
|
|
401
|
+
FilesystemAdapter,
|
|
402
|
+
} from 'strata-storage/capacitor';
|
|
398
403
|
|
|
399
404
|
const storage = defineStorage();
|
|
400
|
-
storage.registerAdapter(new PreferencesAdapter());
|
|
405
|
+
storage.registerAdapter(new PreferencesAdapter()); // UserDefaults / SharedPreferences
|
|
401
406
|
storage.registerAdapter(new SecureStorageAdapter()); // Keychain / EncryptedSharedPreferences
|
|
407
|
+
storage.registerAdapter(new SqliteAdapter()); // native SQLite
|
|
408
|
+
storage.registerAdapter(new FilesystemAdapter()); // native files
|
|
402
409
|
|
|
403
410
|
await storage.set('secret', token, { storage: 'secure' });
|
|
404
411
|
```
|
|
@@ -407,10 +414,25 @@ await storage.set('secret', token, { storage: 'secure' });
|
|
|
407
414
|
|---------|-------------|-----------------|
|
|
408
415
|
| `preferences` | UserDefaults | SharedPreferences |
|
|
409
416
|
| `secure` | Keychain | EncryptedSharedPreferences |
|
|
410
|
-
| `sqlite` | SQLite | SQLite |
|
|
411
|
-
| `filesystem` | FileManager |
|
|
417
|
+
| `sqlite` | SQLite (multi-store) | SQLite (multi-store) |
|
|
418
|
+
| `filesystem` | FileManager | java.io.File |
|
|
412
419
|
|
|
413
|
-
|
|
420
|
+
**SQLite multi-store** (2.6.0+): each `SqliteAdapter` instance binds to a `(database, table)` pair, so distinct logical stores map to distinct physical SQLite files / tables and cannot collide.
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
import { SqliteAdapter } from 'strata-storage/capacitor';
|
|
424
|
+
|
|
425
|
+
const analytics = defineStorage();
|
|
426
|
+
analytics.registerAdapter(new SqliteAdapter({ database: 'analytics', table: 'events' }));
|
|
427
|
+
|
|
428
|
+
const audit = defineStorage();
|
|
429
|
+
audit.registerAdapter(new SqliteAdapter({ database: 'audit', table: 'rows' }));
|
|
430
|
+
// → separate physical .db files; writes to `analytics` can never bleed into `audit`.
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
`await storage.size(true)` aggregates `{ total, count, byStorage, ... }`; native SQLite and filesystem additionally report a per-column byte breakdown (keys / values / metadata) when called on those adapters directly.
|
|
434
|
+
|
|
435
|
+
> **Honest note:** the native iOS/Android adapters depend on your downstream Capacitor project setup and platform configuration, and native behavior cannot be exercised by the web/Node test suite. Follow [`docs/guides/platforms/device-verification.md`](./docs/guides/platforms/device-verification.md) to verify on a real iOS and Android device after integrating.
|
|
414
436
|
|
|
415
437
|
### Firebase (optional cloud sync)
|
|
416
438
|
|
|
@@ -436,8 +458,8 @@ await storage.set('data', value, { storage: 'firestore' });
|
|
|
436
458
|
| `url` | Web | ✅ | async only | Shareable UI state, length-limited |
|
|
437
459
|
| `preferences` | Mobile | ❌ | async only | UserDefaults / SharedPreferences |
|
|
438
460
|
| `secure` | Mobile | ❌ | async only | Keychain / EncryptedSharedPreferences |
|
|
439
|
-
| `sqlite` | Mobile | ❌ | async only | Native SQLite |
|
|
440
|
-
| `filesystem` | Mobile | ❌ | async only | Native files |
|
|
461
|
+
| `sqlite` | Mobile | ❌ | async only | Native SQLite — multi-store via `(database, table)` (2.6.0+) |
|
|
462
|
+
| `filesystem` | Mobile | ❌ | async only | Native files — file-per-key with atomic writes (2.6.0+) |
|
|
441
463
|
|
|
442
464
|
"async only" means encryption and compression require the `await storage.set(...)` path — the synchronous API cannot encrypt or compress.
|
|
443
465
|
|
package/android/AGENTS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AGENTS.md — android/
|
|
2
2
|
|
|
3
|
-
Last Updated: 2026-
|
|
3
|
+
Last Updated: 2026-05-27
|
|
4
4
|
|
|
5
5
|
> Agent instructions for Android native development.
|
|
6
6
|
|
|
@@ -8,27 +8,44 @@ Last Updated: 2026-04-03
|
|
|
8
8
|
|
|
9
9
|
| File | Purpose |
|
|
10
10
|
|------|---------|
|
|
11
|
-
| `StrataStoragePlugin.java` | Capacitor plugin entry (com.stratastorage) |
|
|
12
|
-
| `SharedPreferencesStorage.java` | General key-value storage (com.strata.storage) |
|
|
13
|
-
| `EncryptedStorage.java` | Secure storage (com.strata.storage) |
|
|
14
|
-
| `SQLiteStorage.java` |
|
|
11
|
+
| `StrataStoragePlugin.java` | Capacitor plugin entry (`com.stratastorage`) |
|
|
12
|
+
| `SharedPreferencesStorage.java` | General key-value storage (`com.strata.storage`) |
|
|
13
|
+
| `EncryptedStorage.java` | Secure storage via EncryptedSharedPreferences (`com.strata.storage`); requires `androidx.security:security-crypto 1.1.0` (stable, upgraded in v2.6.0) |
|
|
14
|
+
| `SQLiteStorage.java` | SQLite database storage — multi-store (v2.6.0): `database`/`table` options honoured, full `StorageValue` wrapper round-trip, `size(detailed)` supported (`com.strata.storage`) |
|
|
15
|
+
| `FilesystemStorage.java` | File-per-key native storage under `getFilesDir()/strata_storage/` (new in v2.6.0); atomic writes via staging rename; `isAvailable()` returns `true`; no external permissions needed (`com.strata.storage`) |
|
|
16
|
+
|
|
17
|
+
## v2.6.0 Notes
|
|
18
|
+
|
|
19
|
+
- **`androidx.security 1.1.0`:** upgraded from `1.1.0-alpha06`. Requires `compileSdkVersion 34`. If Gradle sync fails, bump `compileSdkVersion` and `targetSdkVersion` to 34 in `android/app/build.gradle`.
|
|
20
|
+
- **SQLite multi-store:** `call.getString("database")` and `call.getString("table")` are now read and used. Each `(database, table)` pair → distinct `.db` file. Previously ignored.
|
|
21
|
+
- **SQLite value shape:** native `get` returns full `StorageValue` wrapper. Corrupt rows → treated as miss (no throw).
|
|
22
|
+
- **FilesystemStorage.java:** new class. Writes are atomic (staging temp → `renameTo`). `keys()` excludes `.staging/` artifacts. `size(detailed)` → `{ keys, values, metadata }`.
|
|
23
|
+
- **Pending on-device verification** — see `docs/guides/platforms/device-verification.md`.
|
|
15
24
|
|
|
16
25
|
## Agent Rules
|
|
17
26
|
|
|
18
27
|
### Security (IRON-SOLID)
|
|
19
28
|
- Sensitive data MUST use `EncryptedStorage`
|
|
20
29
|
- NEVER store secrets in `SharedPreferencesStorage`
|
|
30
|
+
- `androidx.security:security-crypto` version MUST be `1.1.0` (stable) — not alpha
|
|
21
31
|
|
|
22
32
|
### SQL Safety (IRON-SOLID)
|
|
23
33
|
- ALWAYS use parameterized queries
|
|
24
34
|
- NEVER concatenate user input into SQL strings
|
|
25
35
|
|
|
36
|
+
### Filesystem Safety
|
|
37
|
+
- NEVER read from `strata_storage/.staging/` — staging files are transient
|
|
38
|
+
- Key sanitisation must be consistent between `set`, `get`, `remove`, and `keys`
|
|
39
|
+
|
|
26
40
|
### Plugin Architecture
|
|
27
41
|
- Methods exposed through `StrataStoragePlugin.java`
|
|
28
42
|
- Each storage backend is a separate Java class
|
|
29
43
|
- Two package paths: `com.stratastorage` (plugin) and `com.strata.storage` (impl)
|
|
30
44
|
|
|
45
|
+
### Build
|
|
46
|
+
- `compileSdkVersion 34` required for `androidx.security 1.1.0`
|
|
47
|
+
- Verify Gradle build succeeds after any change
|
|
48
|
+
|
|
31
49
|
### Before Modifying
|
|
32
50
|
- Understand Capacitor plugin protocol for Android
|
|
33
|
-
- Test on Android emulator after changes
|
|
34
|
-
- Verify Gradle build succeeds
|
|
51
|
+
- Test on Android emulator after changes; follow device-verification guide
|
package/android/CLAUDE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CLAUDE.md — android/
|
|
2
2
|
|
|
3
|
-
Last Updated: 2026-
|
|
3
|
+
Last Updated: 2026-05-27
|
|
4
4
|
|
|
5
5
|
## Android Native Plugin
|
|
6
6
|
|
|
@@ -12,8 +12,9 @@ Java implementation of native storage backends for Capacitor.
|
|
|
12
12
|
|------|------|---------|
|
|
13
13
|
| `StrataStoragePlugin.java` | `src/main/java/com/stratastorage/` | Main Capacitor plugin entry |
|
|
14
14
|
| `SharedPreferencesStorage.java` | `src/main/java/com/strata/storage/` | SharedPreferences general storage |
|
|
15
|
-
| `EncryptedStorage.java` | `src/main/java/com/strata/storage/` | EncryptedSharedPreferences secure storage |
|
|
16
|
-
| `SQLiteStorage.java` | `src/main/java/com/strata/storage/` | SQLite database storage |
|
|
15
|
+
| `EncryptedStorage.java` | `src/main/java/com/strata/storage/` | EncryptedSharedPreferences secure storage (`androidx.security 1.1.0`) |
|
|
16
|
+
| `SQLiteStorage.java` | `src/main/java/com/strata/storage/` | SQLite database storage — multi-store (v2.6.0) |
|
|
17
|
+
| `FilesystemStorage.java` | `src/main/java/com/strata/storage/` | File-per-key storage under `Context.getFilesDir()/strata_storage/` (new in v2.6.0) |
|
|
17
18
|
|
|
18
19
|
### Configuration
|
|
19
20
|
|
|
@@ -23,27 +24,64 @@ Java implementation of native storage backends for Capacitor.
|
|
|
23
24
|
|
|
24
25
|
**Note**: There are two Java package paths — `com.stratastorage` (plugin) and `com.strata.storage` (implementations).
|
|
25
26
|
|
|
27
|
+
## v2.6.0 Changes
|
|
28
|
+
|
|
29
|
+
### `androidx.security:security-crypto` 1.1.0 (stable)
|
|
30
|
+
`EncryptedStorage.java` depends on `androidx.security:security-crypto`. The
|
|
31
|
+
dependency was upgraded from `1.1.0-alpha06` to `1.1.0` (stable) in v2.6.0.
|
|
32
|
+
The `EncryptedSharedPreferences` / `MasterKey` API is unchanged. The stable
|
|
33
|
+
version requires `compileSdkVersion 34` — if Gradle sync fails after this
|
|
34
|
+
change, bump `compileSdkVersion` and `targetSdkVersion` in
|
|
35
|
+
`android/app/build.gradle` to 34.
|
|
36
|
+
|
|
37
|
+
### SQLite multi-store
|
|
38
|
+
`SQLiteStorage.java` now accepts `database` and `table` parameters from
|
|
39
|
+
`call.getString("database")` / `call.getString("table")`. Each unique pair
|
|
40
|
+
opens a distinct `.db` file in the app's internal storage. Table identifiers
|
|
41
|
+
are sanitised to `[A-Za-z0-9_]`. The full `StorageValue` wrapper is serialised
|
|
42
|
+
to JSON and stored in a single `value` column; native `get` returns the full
|
|
43
|
+
wrapper so TTL, tags, and metadata survive the round-trip. `size(detailed)`
|
|
44
|
+
returns per-column byte breakdown.
|
|
45
|
+
|
|
46
|
+
### FilesystemStorage.java (new)
|
|
47
|
+
Stores each key as `getFilesDir()/strata_storage/<sanitised-key>.json`. Writes
|
|
48
|
+
are atomic: value is written to `strata_storage/.staging/<key>.tmp`, then
|
|
49
|
+
renamed into place using `renameTo()`. Staging artifacts are excluded from
|
|
50
|
+
`keys()`. No external filesystem permissions are needed (`getFilesDir()` is
|
|
51
|
+
app-private). `isAvailable()` returns `true`. `size(detailed)` supported.
|
|
52
|
+
|
|
53
|
+
> **Pending on-device verification** — native code is complete and reviewed; see
|
|
54
|
+
> `docs/guides/platforms/device-verification.md` for the test matrix.
|
|
55
|
+
|
|
26
56
|
## Rules
|
|
27
57
|
|
|
28
58
|
### Security (IRON-SOLID)
|
|
29
59
|
- Sensitive data MUST use `EncryptedStorage` (EncryptedSharedPreferences)
|
|
30
60
|
- NEVER store credentials, tokens, or secrets in `SharedPreferencesStorage`
|
|
31
61
|
- Follow Android Keystore best practices
|
|
62
|
+
- `androidx.security:security-crypto 1.1.0` (stable) is the required version
|
|
32
63
|
|
|
33
64
|
### SQL Safety (IRON-SOLID)
|
|
34
65
|
- ALWAYS use parameterized queries in `SQLiteStorage`
|
|
35
66
|
- NEVER concatenate user input into SQL strings
|
|
36
67
|
- Use `SQLiteDatabase.query()` or prepared statements
|
|
37
68
|
|
|
69
|
+
### Filesystem Safety
|
|
70
|
+
- NEVER read from `strata_storage/.staging/` — staging files are transient
|
|
71
|
+
- Key sanitisation must be consistent between `set`, `get`, `remove`, and `keys`
|
|
72
|
+
- `getFilesDir()` is app-private; no external storage permissions required
|
|
73
|
+
|
|
38
74
|
### Plugin Architecture
|
|
39
75
|
- All native methods exposed through `StrataStoragePlugin.java`
|
|
40
76
|
- Plugin bridges Capacitor JS calls to Java implementations
|
|
41
77
|
- Each storage backend is a separate Java class
|
|
78
|
+
- Two package paths: `com.stratastorage` (plugin) and `com.strata.storage` (impl)
|
|
42
79
|
|
|
43
80
|
### Build
|
|
44
81
|
- Gradle-based build system
|
|
82
|
+
- `compileSdkVersion 34` required for `androidx.security 1.1.0`
|
|
45
83
|
- Proguard rules configured for release builds
|
|
46
|
-
- Test on Android emulator after changes
|
|
84
|
+
- Test on Android emulator after changes; follow device-verification guide
|
|
47
85
|
|
|
48
86
|
### Before Modifying
|
|
49
87
|
- Understand the Capacitor plugin protocol for Android
|
package/android/build.gradle
CHANGED
|
@@ -51,7 +51,7 @@ dependencies {
|
|
|
51
51
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
|
52
52
|
implementation project(':capacitor-android')
|
|
53
53
|
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
|
54
|
-
implementation 'androidx.security:security-crypto:1.1.0
|
|
54
|
+
implementation 'androidx.security:security-crypto:1.1.0'
|
|
55
55
|
testImplementation "junit:junit:$junitVersion"
|
|
56
56
|
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
|
57
57
|
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
package com.strata.storage;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.util.Log;
|
|
5
|
+
import com.getcapacitor.JSObject;
|
|
6
|
+
import java.io.File;
|
|
7
|
+
import java.io.FileOutputStream;
|
|
8
|
+
import java.io.IOException;
|
|
9
|
+
import java.nio.charset.StandardCharsets;
|
|
10
|
+
import java.net.URLDecoder;
|
|
11
|
+
import java.net.URLEncoder;
|
|
12
|
+
import java.util.ArrayList;
|
|
13
|
+
import java.util.List;
|
|
14
|
+
import java.util.UUID;
|
|
15
|
+
import org.json.JSONException;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Filesystem-backed storage. One file per key lives under
|
|
19
|
+
* {@code <filesDir>/strata_storage}. The file name is a reversible
|
|
20
|
+
* URL-encoding of the key (no path separators survive encoding) and the file
|
|
21
|
+
* contents are the JSON-serialized FULL wrapper object ({@code value, created,
|
|
22
|
+
* updated, expires?, tags?, metadata?}), mirroring the SQLite value-shape
|
|
23
|
+
* contract.
|
|
24
|
+
*
|
|
25
|
+
* <p>Writes are atomic: a temp file is written then renamed over the target.
|
|
26
|
+
* All access is synchronized on the instance so overlapping bridge calls are
|
|
27
|
+
* safe.
|
|
28
|
+
*/
|
|
29
|
+
public class FilesystemStorage {
|
|
30
|
+
private static final String DIR_NAME = "strata_storage";
|
|
31
|
+
private static final String ENCODING = "UTF-8";
|
|
32
|
+
/**
|
|
33
|
+
* Reserved subdirectory (inside the storage dir) that holds in-flight temp
|
|
34
|
+
* files for atomic writes. Because it is a directory, the file-only
|
|
35
|
+
* enumeration in keys()/size()/clear() skips it automatically, and temp
|
|
36
|
+
* names therefore can never collide with an encoded key file in the storage
|
|
37
|
+
* dir (e.g. a real key {@code "backup.tmp"}).
|
|
38
|
+
*/
|
|
39
|
+
private static final String STAGING_DIR_NAME = ".strata-staging";
|
|
40
|
+
|
|
41
|
+
private final File baseDir;
|
|
42
|
+
private final File stagingDir;
|
|
43
|
+
|
|
44
|
+
public FilesystemStorage(Context context) {
|
|
45
|
+
this.baseDir = new File(context.getFilesDir(), DIR_NAME);
|
|
46
|
+
this.stagingDir = new File(baseDir, STAGING_DIR_NAME);
|
|
47
|
+
ensureDir();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private synchronized void ensureDir() {
|
|
51
|
+
if (!baseDir.exists()) {
|
|
52
|
+
// mkdirs() returns false if it already exists due to a race; the
|
|
53
|
+
// subsequent exists() check below is the real guard.
|
|
54
|
+
baseDir.mkdirs();
|
|
55
|
+
}
|
|
56
|
+
if (!stagingDir.exists()) {
|
|
57
|
+
stagingDir.mkdirs();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Reversibly encode a key into a path-separator-free file name. */
|
|
62
|
+
private static String encodeKey(String key) {
|
|
63
|
+
try {
|
|
64
|
+
return URLEncoder.encode(key, ENCODING);
|
|
65
|
+
} catch (Exception e) {
|
|
66
|
+
// UTF-8 is always supported; fall back defensively.
|
|
67
|
+
return key.replaceAll("[^A-Za-z0-9_-]", "_");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Inverse of {@link #encodeKey(String)}. */
|
|
72
|
+
private static String decodeKey(String fileName) {
|
|
73
|
+
try {
|
|
74
|
+
return URLDecoder.decode(fileName, ENCODING);
|
|
75
|
+
} catch (Exception e) {
|
|
76
|
+
return fileName;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private File fileForKey(String key) {
|
|
81
|
+
return new File(baseDir, encodeKey(key));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Read + parse the full wrapper for {@code key}. Missing file → {@code null}.
|
|
86
|
+
* Unparseable contents → {@code null} (treated as a miss; never throws).
|
|
87
|
+
*/
|
|
88
|
+
public synchronized JSObject get(String key) {
|
|
89
|
+
File file = fileForKey(key);
|
|
90
|
+
if (!file.exists() || !file.isFile()) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
String json = readFile(file);
|
|
95
|
+
return new JSObject(json);
|
|
96
|
+
} catch (JSONException parseError) {
|
|
97
|
+
Log.w("StrataStorage", "Unparseable filesystem value for key " + key);
|
|
98
|
+
return null;
|
|
99
|
+
} catch (IOException ioError) {
|
|
100
|
+
Log.e("StrataStorage", "Failed to read filesystem value for key " + key, ioError);
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Atomically persist the full wrapper for {@code key}: write a temp file
|
|
107
|
+
* then rename it over the destination.
|
|
108
|
+
*/
|
|
109
|
+
public synchronized boolean set(String key, JSObject wrapper) {
|
|
110
|
+
if (wrapper == null) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
ensureDir();
|
|
114
|
+
File target = fileForKey(key);
|
|
115
|
+
// Temp file lives in the staging subdir (same filesystem → atomic
|
|
116
|
+
// rename) with a UUID name, so it can never collide with a key file.
|
|
117
|
+
File tmp = new File(stagingDir, UUID.randomUUID().toString() + ".tmp");
|
|
118
|
+
FileOutputStream fos = null;
|
|
119
|
+
try {
|
|
120
|
+
fos = new FileOutputStream(tmp);
|
|
121
|
+
fos.write(wrapper.toString().getBytes(StandardCharsets.UTF_8));
|
|
122
|
+
fos.flush();
|
|
123
|
+
fos.getFD().sync();
|
|
124
|
+
fos.close();
|
|
125
|
+
fos = null;
|
|
126
|
+
|
|
127
|
+
// Rename is atomic on the same filesystem. Remove a stale target
|
|
128
|
+
// first since File.renameTo can fail when the destination exists.
|
|
129
|
+
if (target.exists() && !target.delete()) {
|
|
130
|
+
Log.w("StrataStorage", "Could not delete existing file before rename: " + key);
|
|
131
|
+
}
|
|
132
|
+
if (!tmp.renameTo(target)) {
|
|
133
|
+
tmp.delete();
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
return true;
|
|
137
|
+
} catch (IOException e) {
|
|
138
|
+
Log.e("StrataStorage", "Failed to set filesystem value for key " + key, e);
|
|
139
|
+
if (tmp.exists()) {
|
|
140
|
+
tmp.delete();
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
} finally {
|
|
144
|
+
if (fos != null) {
|
|
145
|
+
try {
|
|
146
|
+
fos.close();
|
|
147
|
+
} catch (IOException ignored) {
|
|
148
|
+
// best-effort close
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
public synchronized boolean remove(String key) {
|
|
155
|
+
File file = fileForKey(key);
|
|
156
|
+
if (!file.exists()) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
return file.delete();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Delete all stored entries. {@code prefix}, when non-null, restricts the
|
|
164
|
+
* delete to keys whose (decoded) name starts with the prefix.
|
|
165
|
+
*/
|
|
166
|
+
public synchronized boolean clear(String prefix) {
|
|
167
|
+
File[] files = baseDir.listFiles();
|
|
168
|
+
if (files == null) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
boolean ok = true;
|
|
172
|
+
for (File file : files) {
|
|
173
|
+
// Skips the staging subdirectory (and any other non-file entry).
|
|
174
|
+
if (!file.isFile()) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (prefix == null || decodeKey(file.getName()).startsWith(prefix)) {
|
|
178
|
+
if (!file.delete()) {
|
|
179
|
+
ok = false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// On a full clear, also drop any orphaned in-flight temp files.
|
|
184
|
+
if (prefix == null) {
|
|
185
|
+
File[] temps = stagingDir.listFiles();
|
|
186
|
+
if (temps != null) {
|
|
187
|
+
for (File t : temps) {
|
|
188
|
+
t.delete();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return ok;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
public synchronized List<String> keys(String pattern) {
|
|
196
|
+
List<String> keys = new ArrayList<>();
|
|
197
|
+
File[] files = baseDir.listFiles();
|
|
198
|
+
if (files == null) {
|
|
199
|
+
return keys;
|
|
200
|
+
}
|
|
201
|
+
for (File file : files) {
|
|
202
|
+
// Skips the staging subdirectory (and any other non-file entry).
|
|
203
|
+
if (!file.isFile()) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
String key = decodeKey(file.getName());
|
|
207
|
+
if (pattern == null || key.contains(pattern)) {
|
|
208
|
+
keys.add(key);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return keys;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
public synchronized boolean has(String key) {
|
|
215
|
+
File file = fileForKey(key);
|
|
216
|
+
return file.exists() && file.isFile();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Size information. {@code total} = {@code keys} + {@code values} (matching
|
|
221
|
+
* the web adapters' convention), {@code values} = Σ file byte sizes,
|
|
222
|
+
* {@code keys} = Σ decoded-key byte lengths, {@code metadata} = Σ length of
|
|
223
|
+
* the parsed {@code metadata} field (0 when absent or unparseable),
|
|
224
|
+
* {@code count} = number of stored files. The detailed breakdown is only
|
|
225
|
+
* surfaced to JS when {@code detailed} is true.
|
|
226
|
+
*/
|
|
227
|
+
public synchronized SQLiteStorage.SizeInfo size(boolean detailed) {
|
|
228
|
+
File[] files = baseDir.listFiles();
|
|
229
|
+
long valuesBytes = 0;
|
|
230
|
+
long keysBytes = 0;
|
|
231
|
+
long metadataBytes = 0;
|
|
232
|
+
int count = 0;
|
|
233
|
+
|
|
234
|
+
if (files != null) {
|
|
235
|
+
for (File file : files) {
|
|
236
|
+
// Skips the staging subdirectory (and any other non-file entry).
|
|
237
|
+
if (!file.isFile()) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
count++;
|
|
241
|
+
valuesBytes += file.length();
|
|
242
|
+
keysBytes += decodeKey(file.getName()).getBytes(StandardCharsets.UTF_8).length;
|
|
243
|
+
if (detailed) {
|
|
244
|
+
metadataBytes += metadataByteLength(file);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (!detailed) {
|
|
250
|
+
return new SQLiteStorage.SizeInfo(keysBytes + valuesBytes, count);
|
|
251
|
+
}
|
|
252
|
+
return new SQLiteStorage.SizeInfo(keysBytes + valuesBytes, count, keysBytes, valuesBytes, metadataBytes);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Parse the file and return the byte length of its {@code metadata} field. */
|
|
256
|
+
private long metadataByteLength(File file) {
|
|
257
|
+
try {
|
|
258
|
+
JSObject wrapper = new JSObject(readFile(file));
|
|
259
|
+
Object metadata = wrapper.opt("metadata");
|
|
260
|
+
if (metadata != null && metadata != org.json.JSONObject.NULL) {
|
|
261
|
+
return metadata.toString().getBytes(StandardCharsets.UTF_8).length;
|
|
262
|
+
}
|
|
263
|
+
} catch (Exception ignored) {
|
|
264
|
+
// Unparseable → contributes 0 metadata bytes.
|
|
265
|
+
}
|
|
266
|
+
return 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private String readFile(File file) throws IOException {
|
|
270
|
+
byte[] data = new byte[(int) file.length()];
|
|
271
|
+
java.io.FileInputStream fis = null;
|
|
272
|
+
try {
|
|
273
|
+
fis = new java.io.FileInputStream(file);
|
|
274
|
+
int offset = 0;
|
|
275
|
+
int read;
|
|
276
|
+
while (offset < data.length
|
|
277
|
+
&& (read = fis.read(data, offset, data.length - offset)) != -1) {
|
|
278
|
+
offset += read;
|
|
279
|
+
}
|
|
280
|
+
} finally {
|
|
281
|
+
if (fis != null) {
|
|
282
|
+
fis.close();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return new String(data, StandardCharsets.UTF_8);
|
|
286
|
+
}
|
|
287
|
+
}
|