Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf0b450cf3
|
||
|
|
df5cbe3332
|
||
|
|
b483e8bd76
|
||
|
|
9bfacfb7fb | ||
|
|
dd5b41e8ca |
No files matched your search
+11
-57
@@ -1,60 +1,14 @@
|
||||
version: 2.1
|
||||
|
||||
setup: true
|
||||
orbs:
|
||||
qqq-orb: kingsrook/qqq-orb@0.6.9
|
||||
|
||||
commands:
|
||||
configure-package-install:
|
||||
steps:
|
||||
- run:
|
||||
name: Keep package installation unattended
|
||||
command: |
|
||||
# These build machines are discarded after the job.
|
||||
sudo install -d /etc/needrestart/conf.d
|
||||
printf '%s\n' '$nrconf{restart} = "l";' | sudo tee /etc/needrestart/conf.d/99-qqq-ci.conf >/dev/null
|
||||
|
||||
qqq-orb: kingsrook/qqq-orb@0.8
|
||||
workflows:
|
||||
test_only:
|
||||
jobs:
|
||||
- qqq-orb/mvn_test_only:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [qqq-maven-registry-credentials]
|
||||
filters:
|
||||
branches:
|
||||
ignore: /(develop|release\/.*)/
|
||||
|
||||
publish_snapshot:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [qqq-maven-registry-credentials]
|
||||
branch_type: snapshot
|
||||
filters:
|
||||
branches:
|
||||
only: [develop]
|
||||
|
||||
publish_release_candidate:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [qqq-maven-registry-credentials]
|
||||
branch_type: release_candidate
|
||||
filters:
|
||||
branches:
|
||||
only: [/release\/.*/]
|
||||
|
||||
publish_release:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [qqq-maven-registry-credentials]
|
||||
branch_type: release
|
||||
filters:
|
||||
branches:
|
||||
ignore: /.*/
|
||||
tags:
|
||||
only: [/^v\d+\.\d+\.\d+$/]
|
||||
setup:
|
||||
jobs:
|
||||
- qqq-orb/generate_pipeline:
|
||||
orb_version: '0.8'
|
||||
filters:
|
||||
branches:
|
||||
ignore: automation/qbit-rc-pending
|
||||
tags:
|
||||
only: /v.*/
|
||||
@@ -0,0 +1,25 @@
|
||||
# Standard QRun release caller (v2, thin contract): every push and manual dispatch delegates to the protected qrun-release-v1 channel. Do not edit per repository.
|
||||
name: qrun-release
|
||||
on:
|
||||
push: {}
|
||||
workflow_dispatch: {}
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
jobs:
|
||||
release:
|
||||
uses: self:KofTwentyTwo/platform-ci/.gitea/workflows/java-docker.yaml@qrun-release-v1
|
||||
with:
|
||||
runner: linux-small
|
||||
docker-runner: container-build
|
||||
browsers: true
|
||||
browser-runner: browser-build
|
||||
postpublish:
|
||||
if: ${{ needs.release.outputs.capture == 'true' }}
|
||||
needs: release
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
uses: self:KofTwentyTwo/platform-ci/.gitea/workflows/qrun-source-version.yaml@qrun-release-v1
|
||||
with:
|
||||
runner: linux-small
|
||||
@@ -0,0 +1,23 @@
|
||||
name: CodeQL
|
||||
'on':
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
- release/**
|
||||
- hotfix/**
|
||||
pull_request: {}
|
||||
schedule:
|
||||
- cron: 41 16 * * 0
|
||||
workflow_dispatch: {}
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
security-events: write
|
||||
packages: read
|
||||
concurrency:
|
||||
group: codeql-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
jobs:
|
||||
analyze:
|
||||
uses: QRun-IO/qqq-orb/.github/workflows/qbit-codeql.yml@c288a8864fe9054f68410e1d78f5863aea4c4d22
|
||||
@@ -1,25 +0,0 @@
|
||||
# Cut the next release candidate (QRun-IO/qqq docs/QRUN-QBIT-REPO-STANDARD.md, C6):
|
||||
# run this workflow by hand, or merge a develop PR labelled `release:rc`. It merges develop into
|
||||
# the release line and the release/* push runs CircleCI publish_release_candidate.
|
||||
name: Release candidate
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: Print the plan without merging or creating branches
|
||||
type: boolean
|
||||
default: false
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches: [develop]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release-rc:
|
||||
if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release:rc'))
|
||||
uses: QRun-IO/.github/.github/workflows/qbit-release-rc.yml@main
|
||||
with:
|
||||
dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
profile: qbit
|
||||
orb_version: '0.8'
|
||||
default_branch: develop
|
||||
java_version: '21'
|
||||
browsers: true
|
||||
api_checks: false
|
||||
docs_only: false
|
||||
release_on_main: false
|
||||
release_tag_policy: v-prefix
|
||||
test_branch_policy: non-release-no-hotfix
|
||||
publish:
|
||||
snapshot: true
|
||||
release_candidate: true
|
||||
release: true
|
||||
hotfix: true
|
||||
asciidoc: false
|
||||
@@ -35,7 +35,7 @@ This file provides guidance to Codex when working with code in this repository.
|
||||
## Dependencies
|
||||
|
||||
- Java 21.
|
||||
- Parent `com.kingsrook:qbit-build-parent:2.0.0` (qqq 4.0.0). The default-active `qqq-snapshot` profile imports `qqq-bom-pom:4.1.0-RC.5` ahead of it, as in qbit-quick-search (ADR-0007). Remove that activation when the parent moves to 4.1.
|
||||
- Parent `com.kingsrook:qbit-build-parent:2.0.0` (qqq 4.0.0). The default-active `qqq-snapshot` profile imports `qqq-bom-pom:4.1.0-RC.4` ahead of it, as in qbit-quick-search (ADR-0007). Remove that activation when the parent moves to 4.1.
|
||||
- Tests: JUnit 5, AssertJ.
|
||||
|
||||
## Release
|
||||
|
||||
+1
-28
@@ -5,31 +5,4 @@ All notable changes to this project are documented here. The format follows Keep
|
||||
## [0.1.0] - Unreleased
|
||||
|
||||
### Added
|
||||
- Expanded the host-neutral in-app help bundle to cover storage fields, actions, widgets and possible values, with live references and a `HelpBundleAssertions` coverage gate. Registered it for QQQ startup sync and HOST overrides; retained inline table/app help for hosts without help-content tables.
|
||||
- Every table explains itself: a help bundle (`help/qbit-object-storage.yaml`, QQQ help-bundle format) says what each `object_storage_*` table is for and how its rows get created; it shows on the table screen and when the table is empty. Applied inline by `ObjectStorageHelp` for hosts without help-content tables; a host's own help is kept.
|
||||
- A Setup card on the Storage home (connections, buckets, header rules, protection rules) with the state and next step of each, resolved the way the runtime resolves them.
|
||||
- A starter changelog `db/liquibase/qbit-object-storage/object-storage-defaults.changelog.sql`: a disabled `default-mutable` header rule. Insert-only, run through real Liquibase in `DefaultsChangelogTest`.
|
||||
- `ObjectStorageTablesGuardrailTest` (each table is used at runtime, seeded or explained, and has help).
|
||||
- The presign limits on the bucket row (`maxPresignGetSeconds`, `maxPresignPutSeconds`) are now read at runtime (`ConfigSync.effectiveMaxPresignGetSeconds` / `Put`) as a lower cap: the effective limit is the smaller of the code config and the row. It applies to presigned GET, PUT, POST, previews and the Create download link action. Previously the row value was never read.
|
||||
- `object_storage_object_version` is documented as reserved and not used yet (the indexer lists current versions only).
|
||||
- `StorageBucket.maxUploadBytes()` in the `api` package: the bucket's upload limit in bytes (null when none), so other qbits need not call `config()`, which returns an internal type.
|
||||
- **The service (P1).** `ObjectStorage.bucket(alias)` and `ObjectStorage.asSystem(caller, runId)`: put (multipart above the part size), get with ranges, head, copy (in parts for very large objects), header and metadata changes, delete (delete marker, private trash, or permanent), restore, list, presigned GET, PUT and POST, public URLs, tags. Key building and URL encoding, the header policy (`object_storage_header_rule`), `PublicUrlResolver`, `StorageChangeListener`, `StorageReferenceProvider`, `StorageAlertSink`.
|
||||
- Configuration in code, upserted into visible `object_storage_*` tables; secret references with an allow-list; per-connection rate limits; per-bucket and per-connection record security locks.
|
||||
- The capability model and the Test connection probe (read-only by default, write tests on request, clock skew, presign host), with seeded provider defaults.
|
||||
- The `object_storage_operation` audit log (presigned URLs are logged without their signature) and protection rules (`READ_ONLY`, `NO_DELETE`, `WARN`, with allowed system callers).
|
||||
- Backups follow-ups SF1 to SF5: `BucketRole.BACKUP`, object lock on put, system-caller permanent delete, probes that skip locked backup buckets, and the system-caller context.
|
||||
- **The index (P2).** `object_storage_object` and `object_storage_folder` kept in step with each indexed bucket: resumable full scan with a mass-removal safety stop, HEAD backlog, write-through on every service write, soft-deleted rows, folder rollups. Orphaned and missing objects from `FieldReferenceProvider`, daily usage snapshots, quota and freshness alerts, the nightly job.
|
||||
- **The Storage app (P3, P4).** Admin -> Storage with a responsive home; record screens with sections, child lists and a thumbnail preview; actions Rescan bucket, Check configuration, Refresh from bucket, Apply header policy (replaces app-specific backfills), Change headers and metadata, Edit tags, Copy, Move or rename, Delete, Restore, Create download link, Create upload link, folder rename and delete, Upload files, Test connection; every one a standard Review and Result with a bulk cap.
|
||||
- `ObjectStorageSchema`: PostgreSQL DDL generated from the table metadata.
|
||||
- Foreign keys on every relation between the qbit tables (`fk_<table>_<column>`), generated by `ObjectStorageSchema`: owned observed state (capabilities, folders, objects, versions, usage) cascades from its bucket or object, `object.folder_id` and `object_reference.object_id` are set null, and a bucket's connection, header and protection rules and operation log restrict. Each key is added by an idempotent block that first records rows whose parent is gone in `qqq_fk_quarantine` and clears (nullable column) or deletes (required column) them, so it also upgrades an existing schema. New indexes `ix_object_storage_bucket_1` (connection_id), `ix_object_storage_header_rule_1` and `ix_object_storage_protection_rule_1` (bucket_id) back the keys. Upgrade: run the generated statements again (or the host's copy of them).
|
||||
- Conformance suite against MinIO and Garage (Testcontainers), service, index and action tests, and the index and actions on PostgreSQL.
|
||||
|
||||
### Changed
|
||||
- Raised the QQQ BOM baseline to released `4.1.0-RC.7` for the core help-bundle registry and coverage assertions.
|
||||
- Raised the QQQ BOM baseline to released `4.1.0-RC.5` and delegated S3 client construction to core `S3ClientFactory.buildClient`. Removed local HTTP client and trust-manager implementations; existing endpoint, region, addressing, checksum, TLS/CA, credentials and timeout settings remain supported.
|
||||
- Clients remain owned by each connection runtime and are rebuilt on reset/reinstall. Retry behavior now follows core/SDK defaults instead of forcing `ADAPTIVE_V2`; hosts can select the SDK retry mode through `AWS_RETRY_MODE` or `aws.retryMode`.
|
||||
- Retained local presigning (including a separate endpoint/key). A request-credentials bridge preserves static session tokens because RC.5 core metadata/factory has no session-token field or credentials-provider hook. This bridge and the existing `${file.PATH}` resolver remain necessary in released RC.7 (no session-token/provider hook or file-reference interpreter); their review deadline is now RC.8 under issue #10.
|
||||
- Added the filesystem module with its unused SFTP/SSH stack excluded. Resolved dependencies otherwise only change QQQ RC.4 to RC.5 and jsoup 1.23.1 to 1.23.2; see [dependency details](docs/S3-FACTORY-MIGRATION.md).
|
||||
|
||||
### Notes
|
||||
- Deliberate differences from the design text: Test connection is two actions (read-only, and with write tests) so the write variant can require `object_storage.admin`; a folder record has no subfolder child list (core does not allow a self join as an exposed join); there is no `withTableNamePrefix` (the `object_storage_` prefix is fixed by the naming rule).
|
||||
- Repository scaffold: `ObjectStorageQBitConfig` and `ObjectStorageQBitProducer` (table prefix `object_storage_`, permissions `object_storage.*`), build, CI and repository standard files.
|
||||
@@ -5,131 +5,38 @@
|
||||
|
||||
> QQQ QBit: S3-compatible object storage service and Admin -> Storage app (browse, upload, presigned links, permission-checked and logged writes).
|
||||
|
||||
S3-compatible storage as a capability for any QQQ app, and an admin app to run it. Design: [QRun-IO/qqq#1089](https://github.com/QRun-IO/qqq/pull/1089).
|
||||
|
||||
1. **The service.** One Java API (`ObjectStorage.bucket("files")`) for put, get, head, copy, delete, restore, list, header and metadata changes, presigned upload and download URLs, public URLs and key building, over configured connections and buckets. Works against AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, DigitalOcean Spaces, Ceph RGW and Garage. A capability model, filled in by a **Test connection** probe, says what each provider can do.
|
||||
2. **The Storage app** (`Admin -> Storage`, optional). Buckets, objects, folders, connections, operations, header rules and protection rules as standard QQQ tables, an index of every indexed bucket, a home with real numbers, and standard Review/Result actions for every write.
|
||||
|
||||
**Status:** P1 (service), P2 (index, references, orphaned and missing objects, audit log) and P3/P4 (the app, read and write) are implemented. Not yet released: see [CHANGELOG.md](CHANGELOG.md).
|
||||
**Status:** design: [QRun-IO/qqq#1089](https://github.com/QRun-IO/qqq/pull/1089); implementation in progress. Nothing is published yet.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Java 21.
|
||||
- QQQ `4.1.0-RC.7` (released BOM), including `qqq-backend-module-filesystem`. `ConnectionRuntime` maps connection settings to core's `S3BackendMetaData` and uses `S3ClientFactory.buildClient`; core owns HTTP, TLS and checksum setup. The unused SFTP/SSH dependency is excluded. See the [dependency diff](docs/S3-FACTORY-MIGRATION.md).
|
||||
- A QQQ backend for the qbit's tables (PostgreSQL is the intended store). `ObjectStorageSchema.postgresScript(config)` generates the DDL from the same metadata the app runs on, for your Liquibase or Flyway migration.
|
||||
- QQQ 4.1 (built against `4.1.0-RC.4`).
|
||||
|
||||
## Install
|
||||
|
||||
Not yet published. Once released, add `com.kingsrook.qbits:qbit-object-storage` and produce the qbit into your `QInstance`:
|
||||
|
||||
```java
|
||||
new ObjectStorageQBitProducer()
|
||||
.withQBitConfig(new ObjectStorageQBitConfig()
|
||||
.withBackendName("rdbms")
|
||||
.withConnection(new StorageConnectionConfig("primary")
|
||||
.withProviderType(ProviderType.MINIO)
|
||||
.withEndpoint("${env.S3_ENDPOINT}")
|
||||
.withPresignEndpoint("${env.S3_PRESIGN_ENDPOINT}") // optional: the host clients reach the store by
|
||||
.withRegion("${env.S3_REGION}")
|
||||
.withAddressingStyle(AddressingStyle.PATH)
|
||||
.withAccessKeyRef("${env.S3_ACCESS_KEY}")
|
||||
.withSecretKeyRef("${env.S3_SECRET_KEY}")
|
||||
.withPresignAccessKeyRef("${env.S3_PRESIGN_ACCESS_KEY}") // a least-privilege key for presigned URLs
|
||||
.withPresignSecretKeyRef("${env.S3_PRESIGN_SECRET_KEY}"))
|
||||
.withBucket(new StorageBucketConfig("files").withConnection("primary").withBucketName("${env.S3_BUCKET_NAME}")
|
||||
.withRole(BucketRole.PUBLIC).withPublicBaseUrl("${env.PUBLIC_BASE_URL}").withIndexed(true).withReferenceTracked(true))
|
||||
.withBucket(new StorageBucketConfig("incoming").withConnection("primary").withBucketName("${env.INCOMING_BUCKET}")
|
||||
.withRole(BucketRole.INCOMING).withIndexed(false))
|
||||
.withReferenceProvider(FieldReferenceProvider.keyPrefix("files", "media", "storageKey"))
|
||||
.withAdminApp(true).withParentAppName("admin"))
|
||||
.withQBitConfig(new ObjectStorageQBitConfig().withBackendName("rdbms"))
|
||||
.produce(qInstance);
|
||||
```
|
||||
|
||||
Connections and buckets come from this code only. Secrets are **references** (`${env.X}`, `${prop.X}`, `${file.PATH}`) that must match `allowedSecretRefPatterns` (default `env.S3_*`, `env.STORAGE_*`, `file./var/run/secrets/storage/*`); no table stores a secret value. At startup the qbit upserts one `object_storage_connection` and one `object_storage_bucket` row per entry, so the tables always show the real configuration.
|
||||
|
||||
Client settings take effect when the qbit configuration is reinstalled or the runtime is reset. Presigning keeps its separate public endpoint and credentials. Retry behavior now follows core/SDK defaults; hosts requiring adaptive retries can set `AWS_RETRY_MODE=adaptive` or `aws.retryMode=adaptive`. Static session tokens remain supported through a small local bridge pending core support.
|
||||
|
||||
## In-app help
|
||||
|
||||
The producer registers [help/qbit-object-storage.yaml](src/main/resources/help/qbit-object-storage.yaml)
|
||||
as a `QBIT` bundle. QQQ syncs it when the host has the help-content tables installed;
|
||||
registration is safe without them, and inline table/app help still explains empty lists.
|
||||
The bundle version is the literal POM version; update both when releasing.
|
||||
|
||||
Hosts can register their own `layer: HOST` bundle with `HelpBundles.register(qInstance, "help/my-host.yaml")`.
|
||||
Use the same entry key and role to override qbit text, keeping live `{{...}}` references where useful.
|
||||
Hosts that rename the optional Storage app should provide an app entry for their configured name.
|
||||
`ObjectStorageHelpBundleTest` checks all owned tables, fields, processes, apps, widgets and
|
||||
possible-value sources using `HelpBundleAssertions`, including valid keys, live references and
|
||||
plain summaries of at most 120 characters. It runs with `./mvnw verify`.
|
||||
|
||||
## Use
|
||||
|
||||
```java
|
||||
StorageBucket files = ObjectStorage.bucket("files");
|
||||
files.put(PutRequest.of("media/42/hero.jpg", bytes).withContentType("image/jpeg")); // headers come from the header policy
|
||||
URL link = files.presignGet(PresignGet.of("media/42/hero.jpg").ttl(Duration.ofMinutes(5)));
|
||||
String publicUrl = files.publicUrl("media/42/hero.jpg");
|
||||
files.delete(DeleteRequest.of("media/42/hero.jpg")); // soft where the bucket allows it
|
||||
|
||||
// a calling qbit that checked its own permission (for example backups):
|
||||
ObjectStorage.asSystem("backups", runId).bucket("backups").put(PutRequest.of(key, stream).withObjectLock(LockMode.COMPLIANCE, until));
|
||||
```
|
||||
|
||||
Every call is synchronous, rate limited per connection, throws `StorageException` with a stable reason (`NOT_FOUND`, `ACCESS_DENIED`, `UNSUPPORTED`, `PRECONDITION_FAILED`, `THROTTLED`, `PROVIDER_ERROR`) and the provider's request id, and every write lands in `object_storage_operation`. Other qbits plug in with `ObjectHeaderPolicy`, `PublicUrlResolver`, `StorageChangeListener`, `StorageReferenceProvider` and `StorageAlertSink` (a no-op unless the Alerts qbit is installed).
|
||||
|
||||
## What is in the app
|
||||
|
||||
| Where | What |
|
||||
|---|---|
|
||||
| Home | Buckets, data health (orphaned and missing objects), recent changes, cache-header coverage, usage trend, connections. Each section says what is missing when it is empty. |
|
||||
| Tables | `object_storage_bucket`, `_object`, `_folder`, `_connection`, `_operation`, `_header_rule`, `_protection_rule` in the nav; capabilities, versions, references and daily usage appear as child lists on their parents. |
|
||||
| Bucket actions | Rescan bucket, Check configuration, Apply header policy, Create upload link |
|
||||
| Object actions | Refresh from bucket, Apply header policy, Change headers and metadata, Edit tags, Copy objects, Move or rename objects, Delete objects, Restore objects, Create download link |
|
||||
| Folder actions | Move or rename folder, Delete folder contents, Upload files to this folder |
|
||||
| Table actions | Upload files |
|
||||
| Connection actions | Test connection, Test connection with write tests |
|
||||
|
||||
Every write is a standard streamed ETL: **Review** (the dry run, with summary lines) then **Result**. Deletes are soft wherever the bucket has versioning or a private trash (Restore undoes them); referenced, protected and object-locked objects are skipped and the Review says why; bulk runs are capped at 10,000 objects unless the user has `object_storage.admin`.
|
||||
|
||||
Permissions: `object_storage.view`, `.write`, `.delete`, `.admin` (admin implies the rest). Per-bucket scoping uses record security locks on the `object_storage_bucket` and `object_storage_connection` security key types (switch off with `withBucketScoping(false)` for a single-administrator app).
|
||||
|
||||
## Setup and starter defaults
|
||||
|
||||
The Storage home has a **Setup** card: connections and buckets (declared in code, synced into their tables), header rules, and protection rules, each with its state and next step. Every table has help (`help/qbit-object-storage.yaml`) saying what it is for and how its rows get created, which also shows when the table is empty.
|
||||
|
||||
The jar ships one optional seed changelog. After the changesets that create the tables (the script from `ObjectStorageSchema.postgresScript`), include it from your master changelog:
|
||||
|
||||
```yaml
|
||||
- include: { file: db/liquibase/qbit-object-storage/object-storage-defaults.changelog.sql }
|
||||
```
|
||||
|
||||
It is insert-only and inserts a **disabled** generic header rule, `default-mutable` (every key, `public, max-age=3600`, priority 1000), so nothing changes on deploy: enable and edit it when you want it. Edits survive re-runs. Protection rules are not seeded (a rule needs a bucket). Presign limits on the bucket row are read at runtime: `maxPresignGetSeconds` and `maxPresignPutSeconds` on the bucket row can lower the code config limits (the effective limit is the smaller of the two, cached up to 15 seconds); the row cannot raise them. `object_storage_object_version` is reserved and not used yet.
|
||||
|
||||
## Deployment notes
|
||||
|
||||
- **Uploads** go through the app server (100 MB per file by default, per bucket `maxUploadBytes`), using QQQ's standard file-upload field, so the app needs `uploadedFileArchiveTableName` set in its Javalin metadata, as for any file upload.
|
||||
- **Previews** are short-lived (5 minute) presigned GETs, so the Next dashboard's `img-src` must allow the storage host (it allows `https:` by default). Video, audio and PDF previews need same-origin media (the dashboard's `media-src` and `frame-src` are `'self'`); a same-origin download route is the planned follow-up. Until then those files show the file card with download and open links.
|
||||
- **Scheduler.** Set `withSchedulerName(...)` to run the nightly scan, HEAD backlog, reference scan, usage snapshot and trash purge. Without it, run Rescan bucket by hand.
|
||||
- **Alerts.** With the Alerts qbit installed, bind a `StorageAlertSink` adapter (`ObjectStorageRuntime.setAlertSink`); without it every condition is still visible on the home and in the tables.
|
||||
|
||||
## Conformance
|
||||
|
||||
`ObjectStorageConformanceSuite` runs the service against a store through Testcontainers on every PR: **MinIO** (the digest-pinned Chainguard build the QQQ tests use, since Docker Hub's `minio/minio` is gone) and **Garage** (digest-pinned). The index and the actions also run on **PostgreSQL**. AWS S3, R2 and B2 run from an owner-run workflow with scratch buckets; Wasabi, Spaces and Ceph results are community reported. See the design for the provider matrix.
|
||||
|
||||
## Demo server
|
||||
|
||||
`src/test/java/.../devserver/StorageDevServer` starts MinIO and an in-memory QQQ instance with generated images, header rules, references, an index and some history, and serves the Next dashboard, for looking at the app in a browser.
|
||||
|
||||
## Naming
|
||||
|
||||
Tables are `object_storage_<name>` (lowercase snake_case, backend name equal to the table name). Permissions are `object_storage.<action>`.
|
||||
|
||||
## Adopting in another app
|
||||
|
||||
Follow the QQQ [adoption checklist](https://github.com/QRun-IO/qqq/blob/develop/docs/migration/adoption-checklist.adoc) once the first release is published.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
./mvnw verify
|
||||
```
|
||||
|
||||
Always use the Maven wrapper (Maven 3.9.11); Maven 3.10 cannot read the parent POM. The tests need Docker (Testcontainers); in CI a missing Docker fails the build instead of skipping.
|
||||
Always use the Maven wrapper (Maven 3.9.11); Maven 3.10 cannot read the parent POM.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# Upgrading
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Nothing is required. Help and the Setup card appear when the qbit is produced; the schema is unchanged.
|
||||
- Optional: include `db/liquibase/qbit-object-storage/object-storage-defaults.changelog.sql` after the changesets that create the tables, to get a disabled generic header rule (`default-mutable`). It is insert-only and changes no behavior until you enable the rule.
|
||||
- Behavior change: the presign limits on an `object_storage_bucket` row (`maxPresignGetSeconds`, `maxPresignPutSeconds`) are now read, as a lower cap only. Any bucket row whose presign values are lower than your code config shortens links after this upgrade (the config sync copied the code values into the row on first start, so a row someone lowered earlier counts). Check the values on your bucket rows; 0 or empty means no row limit, and a value above the code limit has no effect.
|
||||
- `object_storage_object_version` stays empty: it is reserved.
|
||||
@@ -1,86 +0,0 @@
|
||||
# Core S3 factory migration (issue #10, F3)
|
||||
|
||||
Core source was inspected at local QRun-IO/qqq tag `v4.1.0-RC.5`:
|
||||
`qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/{utils/S3ClientFactory,model/metadata/S3BackendMetaData}.java`.
|
||||
|
||||
## Ownership and host behavior
|
||||
|
||||
`ConnectionRuntime` resolves secret/setting references and maps endpoint, region,
|
||||
path style, checksum mode, TLS verification, extra CA PEM, static keys, and both
|
||||
HTTP timeouts into `S3BackendMetaData`. Default-chain and web-identity modes leave
|
||||
keys unset so core uses the SDK default credentials chain. Provider-specific qbit
|
||||
defaults and the existing 5-second connect / 60-second read timeouts are preserved.
|
||||
Core handles custom CA trust in addition to the JVM trust store.
|
||||
|
||||
The qbit calls uncached `buildClient`, because it owns and closes each client.
|
||||
`ObjectStorageRuntime` caches by connection name and resets clients on
|
||||
configuration install/reset; `settingsHash` is an informational table value,
|
||||
not the cache key. This migration keeps that lifecycle and avoids sharing a
|
||||
core-cached client that the qbit would later close. Configuration changes take
|
||||
effect on reinstall/reset, not through mutation of an already cached client.
|
||||
|
||||
Core does not expose retry configuration. Requests now use core/SDK retry
|
||||
defaults instead of the former forced `ADAPTIVE_V2`. Hosts needing adaptive
|
||||
retries can set the SDK's `AWS_RETRY_MODE=adaptive` or `aws.retryMode=adaptive`.
|
||||
The public qbit configuration API is unchanged.
|
||||
|
||||
## Remaining local code
|
||||
|
||||
- Core RC.5 has no session-token field or credentials-provider customization
|
||||
in `S3BackendMetaData` / `S3ClientFactory.buildClient`. The narrow
|
||||
`SessionCredentialsClient` bridge adds session credentials to requests while
|
||||
retaining their overrides; core still creates the client and HTTP transport.
|
||||
Core needs session-token metadata (including enrichment and cache-key support)
|
||||
and `AwsSessionCredentials` construction, or a caller-provided credentials hook.
|
||||
Marker: `STAND-IN(core=QRun-IO/qqq#1165, removeBy=4.1.0-RC.7)`.
|
||||
- Core has no presigner factory. The local presigner retains its separate public
|
||||
endpoint and least-privilege keys, along with existing session-token handling.
|
||||
- `SecretRefs` file resolution is unchanged and carries the same RC.7 marker.
|
||||
|
||||
## Resolved dependency diff
|
||||
|
||||
Captured using the Maven 3.9.11 wrapper's `-B dependency:tree` before and after
|
||||
this change. Coordinates below are normalized to omit tree indentation and
|
||||
parent ordering. Full raw trees are in `target/dependency-tree-{before,after}.txt`
|
||||
for this working session (generated build files, not committed).
|
||||
|
||||
```diff
|
||||
- com.kingsrook.qqq:qqq-backend-core:jar:4.1.0-RC.4:compile
|
||||
- com.kingsrook.qqq:qqq-backend-module-postgres:jar:4.1.0-RC.4:test
|
||||
- com.kingsrook.qqq:qqq-backend-module-rdbms:jar:4.1.0-RC.4:test
|
||||
- com.kingsrook.qqq:qqq-middleware-javalin:jar:4.1.0-RC.4:test
|
||||
- com.kingsrook.qqq:qqq-openapi:jar:4.1.0-RC.4:test
|
||||
- org.jsoup:jsoup:jar:1.23.1:compile
|
||||
+ com.kingsrook.qqq:qqq-backend-core:jar:4.1.0-RC.5:compile
|
||||
+ com.kingsrook.qqq:qqq-backend-module-filesystem:jar:4.1.0-RC.5:compile
|
||||
+ com.kingsrook.qqq:qqq-backend-module-postgres:jar:4.1.0-RC.5:test
|
||||
+ com.kingsrook.qqq:qqq-backend-module-rdbms:jar:4.1.0-RC.5:test
|
||||
+ com.kingsrook.qqq:qqq-middleware-javalin:jar:4.1.0-RC.5:test
|
||||
+ com.kingsrook.qqq:qqq-openapi:jar:4.1.0-RC.5:test
|
||||
+ org.jsoup:jsoup:jar:1.23.2:compile
|
||||
```
|
||||
|
||||
The only new resolved artifact is `qqq-backend-module-filesystem`. It declares
|
||||
`org.apache.sshd:sshd-sftp:2.16.0`, which would bring `sshd-core` and `sshd-common`;
|
||||
that unused SFTP stack is excluded at the qbit dependency. No SSH libraries or
|
||||
AWS SDK v1 artifacts are present in the resulting tree. AWS SDK v2 remains
|
||||
`2.41.34`; `commons-lang3:3.20.0` was already present and is unchanged. The
|
||||
filesystem module's test dependencies do not become runtime dependencies.
|
||||
|
||||
## Verification in this worktree
|
||||
|
||||
Java 21.0.12.1, Maven wrapper 3.9.11; `./mvnw.cmd -B verify`:
|
||||
|
||||
- Checkstyle: zero violations; production and test compilation succeeded.
|
||||
- Surefire: 119 tests reported, 32 passed, 87 skipped, zero failures/errors.
|
||||
All eight new `ConnectionRuntimeTest` tests passed, including actual HTTP
|
||||
signing with a session token on GET, PUT and pagination.
|
||||
- Docker Desktop access is denied at `npipe:////./pipe/dockerDesktopLinuxEngine`.
|
||||
MinIO, Garage, standalone-service and PostgreSQL integration coverage could
|
||||
not run. Testcontainers also reported its default `docker_engine` pipe unavailable.
|
||||
- The full gate **failed** at JaCoCo: instructions 33.68% (required 70%), classes
|
||||
70.75% (required 90%). Coverage thresholds and Docker test policy are unchanged.
|
||||
- No unrelated RC.5 compatibility fixes were needed. `git diff --check` passed.
|
||||
|
||||
Rerun the full verification gate in an environment with Docker access before
|
||||
merging. No changes were committed or pushed.
|
||||
@@ -18,8 +18,6 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<properties>
|
||||
<qbit.guardrails.enforce>true</qbit.guardrails.enforce>
|
||||
<qbit.guardrails.strictUiApiRoutes>true</qbit.guardrails.strictUiApiRoutes>
|
||||
<revision>0.1.0-SNAPSHOT</revision>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
@@ -29,7 +27,7 @@
|
||||
<parent>
|
||||
<groupId>com.kingsrook</groupId>
|
||||
<artifactId>qbit-build-parent</artifactId>
|
||||
<version>2.1.0</version>
|
||||
<version>2.0.0</version>
|
||||
</parent>
|
||||
|
||||
<groupId>com.kingsrook.qbits</groupId>
|
||||
@@ -68,91 +66,7 @@
|
||||
<tag>HEAD</tag>
|
||||
</scm>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<!-- AWS SDK for Java v2; keep in step with qqq-backend-core's awssdk bom. -->
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>bom</artifactId>
|
||||
<version>2.41.34</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-backend-module-filesystem</artifactId>
|
||||
<exclusions>
|
||||
<!-- Only the S3 factory is used; do not ship the SFTP backend's SSH stack. -->
|
||||
<exclusion>
|
||||
<groupId>org.apache.sshd</groupId>
|
||||
<artifactId>sshd-sftp</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>s3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>apache-client</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- only for the demo server in src/test (the Storage app in a browser); not shipped -->
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-middleware-javalin</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- DefaultsChangelogTest runs the shipped seed changelog through real Liquibase on H2 (PostgreSQL mode). -->
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<version>2.3.232</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.liquibase</groupId>
|
||||
<artifactId>liquibase-core</artifactId>
|
||||
<version>4.33.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- the Postgres index test: the real SQL of the object index (src/test only) -->
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-backend-module-rdbms</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-backend-module-postgres</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.7.9</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>testcontainers-postgresql</artifactId>
|
||||
<version>2.0.3</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>testcontainers</artifactId>
|
||||
<version>2.0.3</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-backend-core</artifactId>
|
||||
@@ -173,37 +87,6 @@
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||
<version>3.3.1</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.puppycrawl.tools</groupId>
|
||||
<artifactId>checkstyle</artifactId>
|
||||
<version>10.12.6</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<configuration>
|
||||
<configLocation>checkstyle/config.xml</configLocation>
|
||||
<headerLocation>checkstyle/license.txt</headerLocation>
|
||||
<encoding>UTF-8</encoding>
|
||||
<consoleOutput>true</consoleOutput>
|
||||
<failsOnError>false</failsOnError>
|
||||
<failOnViolation>true</failOnViolation>
|
||||
<violationSeverity>warning</violationSeverity>
|
||||
<includeTestSourceDirectory>false</includeTestSourceDirectory>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>validate</id>
|
||||
<phase>validate</phase>
|
||||
<goals>
|
||||
<goal>check</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
@@ -255,14 +138,14 @@
|
||||
<profiles>
|
||||
<!-- Built against qqq 4.1 (QBitMetaDataProducer). Until qbit-build-parent -->
|
||||
<!-- imports the released 4.1 BOM, this profile is active by default and -->
|
||||
<!-- imports qqq-bom-pom 4.1.0-RC.7 ahead of the parent's 4.0.0 BOM. -->
|
||||
<!-- imports qqq-bom-pom 4.1.0-RC.4 ahead of the parent's 4.0.0 BOM. -->
|
||||
<profile>
|
||||
<id>qqq-snapshot</id>
|
||||
<activation>
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
<properties>
|
||||
<qqq.snapshot.version>4.1.0-RC.7</qqq.snapshot.version>
|
||||
<qqq.snapshot.version>4.1.0-RC.4</qqq.snapshot.version>
|
||||
</properties>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@@ -21,30 +17,17 @@
|
||||
package com.kingsrook.qbits.objectstorage;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qbits.objectstorage.api.ObjectHeaderPolicy;
|
||||
import com.kingsrook.qbits.objectstorage.api.PublicUrlResolver;
|
||||
import com.kingsrook.qbits.objectstorage.api.StorageAlertSink;
|
||||
import com.kingsrook.qbits.objectstorage.api.StorageReferenceProvider;
|
||||
import com.kingsrook.qbits.objectstorage.config.BucketRole;
|
||||
import com.kingsrook.qbits.objectstorage.config.StorageBucketConfig;
|
||||
import com.kingsrook.qbits.objectstorage.config.StorageConnectionConfig;
|
||||
import com.kingsrook.qbits.objectstorage.internal.SecretRefs;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitConfig;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Host-facing configuration for the Object Storage QBit. Code is the source of
|
||||
** ** truth: connections and buckets are declared here and upserted into the
|
||||
** ** object_storage_connection and object_storage_bucket tables.
|
||||
** **
|
||||
** ** Tables are named object_storage_<name> (lowercase snake_case, backend name equal
|
||||
** ** to the table name); permissions are object_storage.<action>.
|
||||
/*******************************************************************************
|
||||
** Host-facing configuration for the Object Storage QBit.
|
||||
**
|
||||
** Tables are named object_storage_<name> (lowercase snake_case, backend name equal
|
||||
** to the table name); permissions are object_storage.<action>.
|
||||
*******************************************************************************/
|
||||
public class ObjectStorageQBitConfig implements QBitConfig
|
||||
{
|
||||
@@ -52,814 +35,16 @@ public class ObjectStorageQBitConfig implements QBitConfig
|
||||
public static final String PERMISSION_BASE_NAME = "object_storage";
|
||||
|
||||
private String backendName;
|
||||
private List<StorageConnectionConfig> connections = new ArrayList<>();
|
||||
private List<StorageBucketConfig> buckets = new ArrayList<>();
|
||||
private ObjectHeaderPolicy headerPolicy;
|
||||
private PublicUrlResolver publicUrlResolver;
|
||||
private List<StorageReferenceProvider> referenceProviders = new ArrayList<>();
|
||||
private StorageAlertSink alertSink;
|
||||
private Boolean adminApp = false;
|
||||
private String appName = "objectStorageAdmin";
|
||||
private String appLabel = "Storage";
|
||||
private String parentAppName;
|
||||
private List<String> allowedSecretRefPatterns = new ArrayList<>(SecretRefs.DEFAULT_ALLOWED_PATTERNS);
|
||||
private Integer bulkCap = 10_000;
|
||||
private Boolean bucketScoping = true;
|
||||
private Boolean openWhenAnonymous = false;
|
||||
private Integer maxPresignsPerUserPerHour = 100;
|
||||
private Double scanRemovalSafetyShare = 0.5;
|
||||
private Long indexMaxKeys = 5_000_000L;
|
||||
private String schedulerName;
|
||||
private Integer nightlyIntervalSeconds = 86400;
|
||||
private Integer headBacklogPerRun = 2000;
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Default constructor.
|
||||
*******************************************************************************/
|
||||
public ObjectStorageQBitConfig()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for backendName.
|
||||
*******************************************************************************/
|
||||
public String getBackendName()
|
||||
{
|
||||
return (this.backendName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for backendName.
|
||||
*******************************************************************************/
|
||||
public void setBackendName(String backendName)
|
||||
{
|
||||
this.backendName = backendName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for backendName.
|
||||
*******************************************************************************/
|
||||
public ObjectStorageQBitConfig withBackendName(String backendName)
|
||||
{
|
||||
this.backendName = backendName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for connections.
|
||||
*******************************************************************************/
|
||||
public List<StorageConnectionConfig> getConnections()
|
||||
{
|
||||
return (this.connections);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for connections.
|
||||
*******************************************************************************/
|
||||
public void setConnections(List<StorageConnectionConfig> connections)
|
||||
{
|
||||
this.connections = connections;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for connections.
|
||||
*******************************************************************************/
|
||||
public ObjectStorageQBitConfig withConnections(List<StorageConnectionConfig> connections)
|
||||
{
|
||||
this.connections = connections;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for buckets.
|
||||
*******************************************************************************/
|
||||
public List<StorageBucketConfig> getBuckets()
|
||||
{
|
||||
return (this.buckets);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for buckets.
|
||||
*******************************************************************************/
|
||||
public void setBuckets(List<StorageBucketConfig> buckets)
|
||||
{
|
||||
this.buckets = buckets;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for buckets.
|
||||
*******************************************************************************/
|
||||
public ObjectStorageQBitConfig withBuckets(List<StorageBucketConfig> buckets)
|
||||
{
|
||||
this.buckets = buckets;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for headerPolicy.
|
||||
*******************************************************************************/
|
||||
public ObjectHeaderPolicy getHeaderPolicy()
|
||||
{
|
||||
return (this.headerPolicy);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for headerPolicy.
|
||||
*******************************************************************************/
|
||||
public void setHeaderPolicy(ObjectHeaderPolicy headerPolicy)
|
||||
{
|
||||
this.headerPolicy = headerPolicy;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for headerPolicy.
|
||||
*******************************************************************************/
|
||||
public ObjectStorageQBitConfig withHeaderPolicy(ObjectHeaderPolicy headerPolicy)
|
||||
{
|
||||
this.headerPolicy = headerPolicy;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for publicUrlResolver.
|
||||
*******************************************************************************/
|
||||
public PublicUrlResolver getPublicUrlResolver()
|
||||
{
|
||||
return (this.publicUrlResolver);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for publicUrlResolver.
|
||||
*******************************************************************************/
|
||||
public void setPublicUrlResolver(PublicUrlResolver publicUrlResolver)
|
||||
{
|
||||
this.publicUrlResolver = publicUrlResolver;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for publicUrlResolver.
|
||||
*******************************************************************************/
|
||||
public ObjectStorageQBitConfig withPublicUrlResolver(PublicUrlResolver publicUrlResolver)
|
||||
{
|
||||
this.publicUrlResolver = publicUrlResolver;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for referenceProviders.
|
||||
*******************************************************************************/
|
||||
public List<StorageReferenceProvider> getReferenceProviders()
|
||||
{
|
||||
return (this.referenceProviders);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for referenceProviders.
|
||||
*******************************************************************************/
|
||||
public void setReferenceProviders(List<StorageReferenceProvider> referenceProviders)
|
||||
{
|
||||
this.referenceProviders = referenceProviders;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for referenceProviders.
|
||||
*******************************************************************************/
|
||||
public ObjectStorageQBitConfig withReferenceProviders(List<StorageReferenceProvider> referenceProviders)
|
||||
{
|
||||
this.referenceProviders = referenceProviders;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for alertSink.
|
||||
*******************************************************************************/
|
||||
public StorageAlertSink getAlertSink()
|
||||
{
|
||||
return (this.alertSink);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for alertSink.
|
||||
*******************************************************************************/
|
||||
public void setAlertSink(StorageAlertSink alertSink)
|
||||
{
|
||||
this.alertSink = alertSink;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for alertSink.
|
||||
*******************************************************************************/
|
||||
public ObjectStorageQBitConfig withAlertSink(StorageAlertSink alertSink)
|
||||
{
|
||||
this.alertSink = alertSink;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for adminApp.
|
||||
*******************************************************************************/
|
||||
public Boolean getAdminApp()
|
||||
{
|
||||
return (this.adminApp);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for adminApp.
|
||||
*******************************************************************************/
|
||||
public void setAdminApp(Boolean adminApp)
|
||||
{
|
||||
this.adminApp = adminApp;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for adminApp.
|
||||
*******************************************************************************/
|
||||
public ObjectStorageQBitConfig withAdminApp(Boolean adminApp)
|
||||
{
|
||||
this.adminApp = adminApp;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for appName.
|
||||
*******************************************************************************/
|
||||
public String getAppName()
|
||||
{
|
||||
return (this.appName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for appName.
|
||||
*******************************************************************************/
|
||||
public void setAppName(String appName)
|
||||
{
|
||||
this.appName = appName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for appName.
|
||||
*******************************************************************************/
|
||||
public ObjectStorageQBitConfig withAppName(String appName)
|
||||
{
|
||||
this.appName = appName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for appLabel.
|
||||
*******************************************************************************/
|
||||
public String getAppLabel()
|
||||
{
|
||||
return (this.appLabel);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for appLabel.
|
||||
*******************************************************************************/
|
||||
public void setAppLabel(String appLabel)
|
||||
{
|
||||
this.appLabel = appLabel;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for appLabel.
|
||||
*******************************************************************************/
|
||||
public ObjectStorageQBitConfig withAppLabel(String appLabel)
|
||||
{
|
||||
this.appLabel = appLabel;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for parentAppName.
|
||||
*******************************************************************************/
|
||||
public String getParentAppName()
|
||||
{
|
||||
return (this.parentAppName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for parentAppName.
|
||||
*******************************************************************************/
|
||||
public void setParentAppName(String parentAppName)
|
||||
{
|
||||
this.parentAppName = parentAppName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for parentAppName.
|
||||
*******************************************************************************/
|
||||
public ObjectStorageQBitConfig withParentAppName(String parentAppName)
|
||||
{
|
||||
this.parentAppName = parentAppName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for allowedSecretRefPatterns.
|
||||
*******************************************************************************/
|
||||
public List<String> getAllowedSecretRefPatterns()
|
||||
{
|
||||
return (this.allowedSecretRefPatterns);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for allowedSecretRefPatterns.
|
||||
*******************************************************************************/
|
||||
public void setAllowedSecretRefPatterns(List<String> allowedSecretRefPatterns)
|
||||
{
|
||||
this.allowedSecretRefPatterns = allowedSecretRefPatterns;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for allowedSecretRefPatterns.
|
||||
*******************************************************************************/
|
||||
public ObjectStorageQBitConfig withAllowedSecretRefPatterns(List<String> allowedSecretRefPatterns)
|
||||
{
|
||||
this.allowedSecretRefPatterns = allowedSecretRefPatterns;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for bulkCap.
|
||||
*******************************************************************************/
|
||||
public Integer getBulkCap()
|
||||
{
|
||||
return (this.bulkCap);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for bulkCap.
|
||||
*******************************************************************************/
|
||||
public void setBulkCap(Integer bulkCap)
|
||||
{
|
||||
this.bulkCap = bulkCap;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for bulkCap.
|
||||
*******************************************************************************/
|
||||
public ObjectStorageQBitConfig withBulkCap(Integer bulkCap)
|
||||
{
|
||||
this.bulkCap = bulkCap;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for bucketScoping.
|
||||
*******************************************************************************/
|
||||
public Boolean getBucketScoping()
|
||||
{
|
||||
return (this.bucketScoping);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for bucketScoping.
|
||||
*******************************************************************************/
|
||||
public void setBucketScoping(Boolean bucketScoping)
|
||||
{
|
||||
this.bucketScoping = bucketScoping;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for bucketScoping.
|
||||
*******************************************************************************/
|
||||
public ObjectStorageQBitConfig withBucketScoping(Boolean bucketScoping)
|
||||
{
|
||||
this.bucketScoping = bucketScoping;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for openWhenAnonymous.
|
||||
*******************************************************************************/
|
||||
public Boolean getOpenWhenAnonymous()
|
||||
{
|
||||
return (this.openWhenAnonymous);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for openWhenAnonymous.
|
||||
*******************************************************************************/
|
||||
public void setOpenWhenAnonymous(Boolean openWhenAnonymous)
|
||||
{
|
||||
this.openWhenAnonymous = openWhenAnonymous;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for openWhenAnonymous.
|
||||
*******************************************************************************/
|
||||
public ObjectStorageQBitConfig withOpenWhenAnonymous(Boolean openWhenAnonymous)
|
||||
{
|
||||
this.openWhenAnonymous = openWhenAnonymous;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for maxPresignsPerUserPerHour.
|
||||
*******************************************************************************/
|
||||
public Integer getMaxPresignsPerUserPerHour()
|
||||
{
|
||||
return (this.maxPresignsPerUserPerHour);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for maxPresignsPerUserPerHour.
|
||||
*******************************************************************************/
|
||||
public void setMaxPresignsPerUserPerHour(Integer maxPresignsPerUserPerHour)
|
||||
{
|
||||
this.maxPresignsPerUserPerHour = maxPresignsPerUserPerHour;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for maxPresignsPerUserPerHour.
|
||||
*******************************************************************************/
|
||||
public ObjectStorageQBitConfig withMaxPresignsPerUserPerHour(Integer maxPresignsPerUserPerHour)
|
||||
{
|
||||
this.maxPresignsPerUserPerHour = maxPresignsPerUserPerHour;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for scanRemovalSafetyShare.
|
||||
*******************************************************************************/
|
||||
public Double getScanRemovalSafetyShare()
|
||||
{
|
||||
return (this.scanRemovalSafetyShare);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for scanRemovalSafetyShare.
|
||||
*******************************************************************************/
|
||||
public void setScanRemovalSafetyShare(Double scanRemovalSafetyShare)
|
||||
{
|
||||
this.scanRemovalSafetyShare = scanRemovalSafetyShare;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for scanRemovalSafetyShare.
|
||||
*******************************************************************************/
|
||||
public ObjectStorageQBitConfig withScanRemovalSafetyShare(Double scanRemovalSafetyShare)
|
||||
{
|
||||
this.scanRemovalSafetyShare = scanRemovalSafetyShare;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for indexMaxKeys.
|
||||
*******************************************************************************/
|
||||
public Long getIndexMaxKeys()
|
||||
{
|
||||
return (this.indexMaxKeys);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for indexMaxKeys.
|
||||
*******************************************************************************/
|
||||
public void setIndexMaxKeys(Long indexMaxKeys)
|
||||
{
|
||||
this.indexMaxKeys = indexMaxKeys;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for indexMaxKeys.
|
||||
*******************************************************************************/
|
||||
public ObjectStorageQBitConfig withIndexMaxKeys(Long indexMaxKeys)
|
||||
{
|
||||
this.indexMaxKeys = indexMaxKeys;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for schedulerName.
|
||||
*******************************************************************************/
|
||||
public String getSchedulerName()
|
||||
{
|
||||
return (this.schedulerName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for schedulerName.
|
||||
*******************************************************************************/
|
||||
public void setSchedulerName(String schedulerName)
|
||||
{
|
||||
this.schedulerName = schedulerName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for schedulerName.
|
||||
*******************************************************************************/
|
||||
public ObjectStorageQBitConfig withSchedulerName(String schedulerName)
|
||||
{
|
||||
this.schedulerName = schedulerName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for nightlyIntervalSeconds.
|
||||
*******************************************************************************/
|
||||
public Integer getNightlyIntervalSeconds()
|
||||
{
|
||||
return (this.nightlyIntervalSeconds);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for nightlyIntervalSeconds.
|
||||
*******************************************************************************/
|
||||
public void setNightlyIntervalSeconds(Integer nightlyIntervalSeconds)
|
||||
{
|
||||
this.nightlyIntervalSeconds = nightlyIntervalSeconds;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for nightlyIntervalSeconds.
|
||||
*******************************************************************************/
|
||||
public ObjectStorageQBitConfig withNightlyIntervalSeconds(Integer nightlyIntervalSeconds)
|
||||
{
|
||||
this.nightlyIntervalSeconds = nightlyIntervalSeconds;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for headBacklogPerRun.
|
||||
*******************************************************************************/
|
||||
public Integer getHeadBacklogPerRun()
|
||||
{
|
||||
return (this.headBacklogPerRun);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for headBacklogPerRun.
|
||||
*******************************************************************************/
|
||||
public void setHeadBacklogPerRun(Integer headBacklogPerRun)
|
||||
{
|
||||
this.headBacklogPerRun = headBacklogPerRun;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for headBacklogPerRun.
|
||||
*******************************************************************************/
|
||||
public ObjectStorageQBitConfig withHeadBacklogPerRun(Integer headBacklogPerRun)
|
||||
{
|
||||
this.headBacklogPerRun = headBacklogPerRun;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Add a connection.
|
||||
*******************************************************************************/
|
||||
public ObjectStorageQBitConfig withConnection(StorageConnectionConfig connection)
|
||||
{
|
||||
this.connections.add(connection);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Add a bucket.
|
||||
*******************************************************************************/
|
||||
public ObjectStorageQBitConfig withBucket(StorageBucketConfig bucket)
|
||||
{
|
||||
this.buckets.add(bucket);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Add a reference provider.
|
||||
*******************************************************************************/
|
||||
public ObjectStorageQBitConfig withReferenceProvider(StorageReferenceProvider provider)
|
||||
{
|
||||
this.referenceProviders.add(provider);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The configured connection of a name, or null.
|
||||
*******************************************************************************/
|
||||
public StorageConnectionConfig findConnection(String name)
|
||||
{
|
||||
for(StorageConnectionConfig connection : connections)
|
||||
{
|
||||
if(connection.getName() != null && connection.getName().equals(name))
|
||||
{
|
||||
return (connection);
|
||||
}
|
||||
}
|
||||
return (null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The configured bucket of an alias, or null.
|
||||
*******************************************************************************/
|
||||
public StorageBucketConfig findBucket(String alias)
|
||||
{
|
||||
for(StorageBucketConfig bucket : buckets)
|
||||
{
|
||||
if(bucket.getAlias() != null && bucket.getAlias().equals(alias))
|
||||
{
|
||||
return (bucket);
|
||||
}
|
||||
}
|
||||
return (null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Validate: names, references to connections, the BACKUP and trash rules, and
|
||||
** the secret-reference allow-list.
|
||||
** The backend must be named.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void validate(QInstance qInstance, List<String> errors)
|
||||
{
|
||||
assertCondition(StringUtils.hasContent(backendName), "backendName is required for the Object Storage QBit", errors);
|
||||
|
||||
Set<String> connectionNames = new HashSet<>();
|
||||
for(StorageConnectionConfig connection : connections)
|
||||
{
|
||||
if(!assertCondition(StringUtils.hasContent(connection.getName()), "Every storage connection needs a name", errors))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
assertCondition(connectionNames.add(connection.getName()), "Duplicate storage connection name: " + connection.getName(), errors);
|
||||
for(String ref : new String[] { connection.getAccessKeyRef(), connection.getSecretKeyRef(), connection.getSessionTokenRef(),
|
||||
connection.getPresignAccessKeyRef(), connection.getPresignSecretKeyRef(), connection.getCaBundleRef() })
|
||||
{
|
||||
assertCondition(SecretRefs.isAllowed(ref, allowedSecretRefPatterns),
|
||||
"Connection " + connection.getName() + ": a secret reference is not an allowed reference (allowed: " + allowedSecretRefPatterns + ")", errors);
|
||||
}
|
||||
if(Boolean.FALSE.equals(connection.getTlsVerify()) && StringUtils.hasContent(connection.getEndpoint()))
|
||||
{
|
||||
assertCondition(connection.getEndpoint().startsWith("http://") || connection.getEndpoint().startsWith("${"),
|
||||
"Connection " + connection.getName() + ": tlsVerify=false is allowed only for http:// development endpoints", errors);
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> aliases = new HashSet<>();
|
||||
for(StorageBucketConfig bucket : buckets)
|
||||
{
|
||||
if(!assertCondition(StringUtils.hasContent(bucket.getAlias()), "Every storage bucket needs an alias", errors))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
assertCondition(aliases.add(bucket.getAlias()), "Duplicate storage bucket alias: " + bucket.getAlias(), errors);
|
||||
assertCondition(connectionNames.contains(bucket.getConnection()), "Bucket " + bucket.getAlias() + " names an unknown connection: " + bucket.getConnection(), errors);
|
||||
assertCondition(StringUtils.hasContent(bucket.getBucketName()), "Bucket " + bucket.getAlias() + " needs a bucketName", errors);
|
||||
if(bucket.getRole() == BucketRole.BACKUP)
|
||||
{
|
||||
assertCondition(!StringUtils.hasContent(bucket.getPublicBaseUrl()), "Bucket " + bucket.getAlias() + " has role BACKUP, which is always private: it cannot have a public base URL", errors);
|
||||
}
|
||||
if(Boolean.TRUE.equals(bucket.getTrashEnabled()))
|
||||
{
|
||||
StorageBucketConfig trashTarget = StringUtils.hasContent(bucket.getTrashBucket()) ? findBucket(bucket.getTrashBucket()) : bucket;
|
||||
if(trashTarget == null)
|
||||
{
|
||||
errors.add("Bucket " + bucket.getAlias() + " names an unknown trash bucket: " + bucket.getTrashBucket());
|
||||
}
|
||||
else
|
||||
{
|
||||
assertCondition(trashTarget.getRole() != BucketRole.PUBLIC && !StringUtils.hasContent(trashTarget.getPublicBaseUrl()),
|
||||
"Bucket " + bucket.getAlias() + ": the trash location must be a private bucket (a trash prefix in a public bucket would leave deleted files readable)", errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -873,4 +58,35 @@ public class ObjectStorageQBitConfig implements QBitConfig
|
||||
return (backendName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for backendName
|
||||
*******************************************************************************/
|
||||
public String getBackendName()
|
||||
{
|
||||
return (this.backendName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for backendName
|
||||
*******************************************************************************/
|
||||
public void setBackendName(String backendName)
|
||||
{
|
||||
this.backendName = backendName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for backendName
|
||||
*******************************************************************************/
|
||||
public ObjectStorageQBitConfig withBackendName(String backendName)
|
||||
{
|
||||
this.backendName = backendName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,15 +1,11 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@@ -21,36 +17,13 @@
|
||||
package com.kingsrook.qbits.objectstorage;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.objectstorage.app.StorageWidgets;
|
||||
import com.kingsrook.qbits.objectstorage.internal.ConfigSync;
|
||||
import com.kingsrook.qbits.objectstorage.internal.DefaultObjectHeaderPolicy;
|
||||
import com.kingsrook.qbits.objectstorage.internal.ObjectStorageRuntime;
|
||||
import com.kingsrook.qbits.objectstorage.model.ObjectStorageHelp;
|
||||
import com.kingsrook.qbits.objectstorage.model.ObjectStorageTables;
|
||||
import com.kingsrook.qbits.objectstorage.processes.ObjectStorageProcesses;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.help.bundles.HelpBundles;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaDataProducer;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Produces the Object Storage qbit: the object_storage_* tables and possible
|
||||
** values, the security key types for per-bucket and per-connection scoping, the
|
||||
** processes (table and record actions), and, when withAdminApp(true), the
|
||||
** Admin -> Storage app with its home widget. Without the admin app a headless
|
||||
** consumer (for example the CMS qbit) still gets the service and the tables.
|
||||
** Produces the Object Storage QBit. Component producers in this package are found by
|
||||
** package scanning.
|
||||
*******************************************************************************/
|
||||
public class ObjectStorageQBitProducer implements QBitMetaDataProducer<ObjectStorageQBitConfig>
|
||||
{
|
||||
@@ -59,7 +32,7 @@ public class ObjectStorageQBitProducer implements QBitMetaDataProducer<ObjectSto
|
||||
public static final String VERSION = "0.1.0";
|
||||
|
||||
private ObjectStorageQBitConfig qBitConfig;
|
||||
private String namespace;
|
||||
private String namespace;
|
||||
|
||||
|
||||
|
||||
@@ -127,77 +100,4 @@ public class ObjectStorageQBitProducer implements QBitMetaDataProducer<ObjectSto
|
||||
return (qBitConfig != null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Build everything and add it to the instance.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public MetaDataProducerMultiOutput produce(QInstance qInstance) throws QException
|
||||
{
|
||||
qBitConfig.validate(qInstance);
|
||||
QBitMetaData qBit = getQBitMetaData();
|
||||
qInstance.addQBit(qBit);
|
||||
ObjectStorageRuntime.install(qBitConfig);
|
||||
ConfigSync.reset();
|
||||
DefaultObjectHeaderPolicy.clearCache();
|
||||
|
||||
MetaDataProducerMultiOutput output = new MetaDataProducerMultiOutput();
|
||||
for(QPossibleValueSource possibleValueSource : ObjectStorageTables.possibleValueSources())
|
||||
{
|
||||
output.add(possibleValueSource);
|
||||
}
|
||||
if(Boolean.TRUE.equals(qBitConfig.getBucketScoping()))
|
||||
{
|
||||
for(QSecurityKeyType keyType : ObjectStorageTables.securityKeyTypes())
|
||||
{
|
||||
output.add(keyType);
|
||||
}
|
||||
}
|
||||
for(QJoinMetaData join : ObjectStorageTables.joins())
|
||||
{
|
||||
output.add(join);
|
||||
}
|
||||
for(QWidgetMetaData widget : ObjectStorageTables.childListWidgets())
|
||||
{
|
||||
output.add(widget);
|
||||
}
|
||||
List<QTableMetaData> tables = new ArrayList<>();
|
||||
for(QTableMetaData table : ObjectStorageTables.tables(qBitConfig))
|
||||
{
|
||||
table.setSourceQBitName(qBit.getName());
|
||||
tables.add(table);
|
||||
output.add(table);
|
||||
}
|
||||
for(QProcessMetaData process : ObjectStorageProcesses.processes(qBitConfig))
|
||||
{
|
||||
process.setSourceQBitName(qBit.getName());
|
||||
output.add(process);
|
||||
}
|
||||
if(Boolean.TRUE.equals(qBitConfig.getAdminApp()))
|
||||
{
|
||||
for(QWidgetMetaData widget : StorageWidgets.widgets())
|
||||
{
|
||||
output.add(widget);
|
||||
}
|
||||
QAppMetaData app = StorageWidgets.app(qBitConfig, tables);
|
||||
if(qBitConfig.getParentAppName() != null)
|
||||
{
|
||||
app.setParentAppName(qBitConfig.getParentAppName());
|
||||
}
|
||||
output.add(app);
|
||||
}
|
||||
output.addSelfToInstance(qInstance);
|
||||
if(Boolean.TRUE.equals(qBitConfig.getAdminApp()) && qBitConfig.getParentAppName() != null && qInstance.getApp(qBitConfig.getParentAppName()) != null)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Admin -> Storage: the parent app lists the Storage app in one of its sections //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
qInstance.getApp(qBitConfig.getParentAppName()).withChild(qInstance.getApp(qBitConfig.getAppName()));
|
||||
}
|
||||
// Registration is metadata-only; core skips startup sync without a helpContent table.
|
||||
HelpBundles.register(qInstance, "help/qbit-object-storage.yaml");
|
||||
ObjectStorageHelp.apply(qInstance, qBitConfig);
|
||||
return (new MetaDataProducerMultiOutput());
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** A bucket alias and key, the result of parsing a public URL.
|
||||
*******************************************************************************/
|
||||
public class BucketKey
|
||||
{
|
||||
private String bucketAlias;
|
||||
private String key;
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Default constructor.
|
||||
*******************************************************************************/
|
||||
public BucketKey()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for bucketAlias.
|
||||
*******************************************************************************/
|
||||
public String getBucketAlias()
|
||||
{
|
||||
return (this.bucketAlias);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for bucketAlias.
|
||||
*******************************************************************************/
|
||||
public void setBucketAlias(String bucketAlias)
|
||||
{
|
||||
this.bucketAlias = bucketAlias;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for bucketAlias.
|
||||
*******************************************************************************/
|
||||
public BucketKey withBucketAlias(String bucketAlias)
|
||||
{
|
||||
this.bucketAlias = bucketAlias;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for key.
|
||||
*******************************************************************************/
|
||||
public String getKey()
|
||||
{
|
||||
return (this.key);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for key.
|
||||
*******************************************************************************/
|
||||
public void setKey(String key)
|
||||
{
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for key.
|
||||
*******************************************************************************/
|
||||
public BucketKey withKey(String key)
|
||||
{
|
||||
this.key = key;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor.
|
||||
*******************************************************************************/
|
||||
public BucketKey(String bucketAlias, String key)
|
||||
{
|
||||
this.bucketAlias = bucketAlias;
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** A feature a connection or bucket may or may not support. The probe fills the status.
|
||||
*******************************************************************************/
|
||||
public enum Capability
|
||||
{
|
||||
LIST_V2,
|
||||
PUT_GET_DELETE,
|
||||
COPY_REPLACE_METADATA,
|
||||
BATCH_DELETE,
|
||||
MULTIPART,
|
||||
PRESIGNED_GET,
|
||||
PRESIGNED_PUT,
|
||||
PRESIGNED_POST,
|
||||
CONDITIONAL_WRITE,
|
||||
VERSIONING,
|
||||
OBJECT_LOCK,
|
||||
LIFECYCLE,
|
||||
BUCKET_POLICY,
|
||||
CORS,
|
||||
OBJECT_TAGGING,
|
||||
ACL,
|
||||
USAGE_ADMIN_API,
|
||||
QUOTA_ADMIN_API
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Probe result for a capability.
|
||||
*******************************************************************************/
|
||||
public enum CapabilityStatus
|
||||
{
|
||||
SUPPORTED,
|
||||
UNSUPPORTED,
|
||||
NOT_PERMITTED,
|
||||
PARTIAL,
|
||||
UNKNOWN
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** The kind of write a change listener is told about.
|
||||
*******************************************************************************/
|
||||
public enum ChangeOperation
|
||||
{
|
||||
PUT,
|
||||
COPY,
|
||||
HEADERS,
|
||||
DELETE,
|
||||
RESTORE
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** A copy of one object to another key or bucket.
|
||||
*******************************************************************************/
|
||||
public class CopyRequest
|
||||
{
|
||||
private String sourceKey;
|
||||
private String destinationKey;
|
||||
private String destinationBucket;
|
||||
private MetadataDirective directive = MetadataDirective.COPY;
|
||||
private ObjectHeaders headers;
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Default constructor.
|
||||
*******************************************************************************/
|
||||
public CopyRequest()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for sourceKey.
|
||||
*******************************************************************************/
|
||||
public String getSourceKey()
|
||||
{
|
||||
return (this.sourceKey);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for sourceKey.
|
||||
*******************************************************************************/
|
||||
public void setSourceKey(String sourceKey)
|
||||
{
|
||||
this.sourceKey = sourceKey;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for sourceKey.
|
||||
*******************************************************************************/
|
||||
public CopyRequest withSourceKey(String sourceKey)
|
||||
{
|
||||
this.sourceKey = sourceKey;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for destinationKey.
|
||||
*******************************************************************************/
|
||||
public String getDestinationKey()
|
||||
{
|
||||
return (this.destinationKey);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for destinationKey.
|
||||
*******************************************************************************/
|
||||
public void setDestinationKey(String destinationKey)
|
||||
{
|
||||
this.destinationKey = destinationKey;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for destinationKey.
|
||||
*******************************************************************************/
|
||||
public CopyRequest withDestinationKey(String destinationKey)
|
||||
{
|
||||
this.destinationKey = destinationKey;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for destinationBucket.
|
||||
*******************************************************************************/
|
||||
public String getDestinationBucket()
|
||||
{
|
||||
return (this.destinationBucket);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for destinationBucket.
|
||||
*******************************************************************************/
|
||||
public void setDestinationBucket(String destinationBucket)
|
||||
{
|
||||
this.destinationBucket = destinationBucket;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for destinationBucket.
|
||||
*******************************************************************************/
|
||||
public CopyRequest withDestinationBucket(String destinationBucket)
|
||||
{
|
||||
this.destinationBucket = destinationBucket;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for directive.
|
||||
*******************************************************************************/
|
||||
public MetadataDirective getDirective()
|
||||
{
|
||||
return (this.directive);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for directive.
|
||||
*******************************************************************************/
|
||||
public void setDirective(MetadataDirective directive)
|
||||
{
|
||||
this.directive = directive;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for directive.
|
||||
*******************************************************************************/
|
||||
public CopyRequest withDirective(MetadataDirective directive)
|
||||
{
|
||||
this.directive = directive;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for headers.
|
||||
*******************************************************************************/
|
||||
public ObjectHeaders getHeaders()
|
||||
{
|
||||
return (this.headers);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for headers.
|
||||
*******************************************************************************/
|
||||
public void setHeaders(ObjectHeaders headers)
|
||||
{
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for headers.
|
||||
*******************************************************************************/
|
||||
public CopyRequest withHeaders(ObjectHeaders headers)
|
||||
{
|
||||
this.headers = headers;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A copy from one key to another in the same bucket.
|
||||
*******************************************************************************/
|
||||
public static CopyRequest of(String sourceKey, String destinationKey)
|
||||
{
|
||||
return (new CopyRequest().withSourceKey(sourceKey).withDestinationKey(destinationKey));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Set the metadata directive.
|
||||
*******************************************************************************/
|
||||
public CopyRequest withMetadata(MetadataDirective directive)
|
||||
{
|
||||
this.directive = directive;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** What a delete actually did.
|
||||
*******************************************************************************/
|
||||
public enum DeleteMode
|
||||
{
|
||||
SOFT_DELETE_MARKER,
|
||||
SOFT_TRASH,
|
||||
PERMANENT,
|
||||
SKIPPED_OBJECT_LOCK,
|
||||
NOT_FOUND
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** A delete of one object key.
|
||||
*******************************************************************************/
|
||||
public class DeleteRequest
|
||||
{
|
||||
private String key;
|
||||
private Boolean permanent = false;
|
||||
private Boolean allowPermanentWhenNoSoftDelete = false;
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Default constructor.
|
||||
*******************************************************************************/
|
||||
public DeleteRequest()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for key.
|
||||
*******************************************************************************/
|
||||
public String getKey()
|
||||
{
|
||||
return (this.key);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for key.
|
||||
*******************************************************************************/
|
||||
public void setKey(String key)
|
||||
{
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for key.
|
||||
*******************************************************************************/
|
||||
public DeleteRequest withKey(String key)
|
||||
{
|
||||
this.key = key;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for permanent.
|
||||
*******************************************************************************/
|
||||
public Boolean getPermanent()
|
||||
{
|
||||
return (this.permanent);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for permanent.
|
||||
*******************************************************************************/
|
||||
public void setPermanent(Boolean permanent)
|
||||
{
|
||||
this.permanent = permanent;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for permanent.
|
||||
*******************************************************************************/
|
||||
public DeleteRequest withPermanent(Boolean permanent)
|
||||
{
|
||||
this.permanent = permanent;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for allowPermanentWhenNoSoftDelete.
|
||||
*******************************************************************************/
|
||||
public Boolean getAllowPermanentWhenNoSoftDelete()
|
||||
{
|
||||
return (this.allowPermanentWhenNoSoftDelete);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for allowPermanentWhenNoSoftDelete.
|
||||
*******************************************************************************/
|
||||
public void setAllowPermanentWhenNoSoftDelete(Boolean allowPermanentWhenNoSoftDelete)
|
||||
{
|
||||
this.allowPermanentWhenNoSoftDelete = allowPermanentWhenNoSoftDelete;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for allowPermanentWhenNoSoftDelete.
|
||||
*******************************************************************************/
|
||||
public DeleteRequest withAllowPermanentWhenNoSoftDelete(Boolean allowPermanentWhenNoSoftDelete)
|
||||
{
|
||||
this.allowPermanentWhenNoSoftDelete = allowPermanentWhenNoSoftDelete;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A delete of a key.
|
||||
*******************************************************************************/
|
||||
public static DeleteRequest of(String key)
|
||||
{
|
||||
return (new DeleteRequest().withKey(key));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Permanently delete every version and delete marker of the key (system callers only).
|
||||
*******************************************************************************/
|
||||
public DeleteRequest permanent()
|
||||
{
|
||||
this.permanent = true;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** The result of a delete.
|
||||
*******************************************************************************/
|
||||
public class DeleteResult
|
||||
{
|
||||
private String key;
|
||||
private DeleteMode mode;
|
||||
private Integer versionsDeleted = 0;
|
||||
private String detail;
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Default constructor.
|
||||
*******************************************************************************/
|
||||
public DeleteResult()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for key.
|
||||
*******************************************************************************/
|
||||
public String getKey()
|
||||
{
|
||||
return (this.key);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for key.
|
||||
*******************************************************************************/
|
||||
public void setKey(String key)
|
||||
{
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for key.
|
||||
*******************************************************************************/
|
||||
public DeleteResult withKey(String key)
|
||||
{
|
||||
this.key = key;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for mode.
|
||||
*******************************************************************************/
|
||||
public DeleteMode getMode()
|
||||
{
|
||||
return (this.mode);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for mode.
|
||||
*******************************************************************************/
|
||||
public void setMode(DeleteMode mode)
|
||||
{
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for mode.
|
||||
*******************************************************************************/
|
||||
public DeleteResult withMode(DeleteMode mode)
|
||||
{
|
||||
this.mode = mode;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for versionsDeleted.
|
||||
*******************************************************************************/
|
||||
public Integer getVersionsDeleted()
|
||||
{
|
||||
return (this.versionsDeleted);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for versionsDeleted.
|
||||
*******************************************************************************/
|
||||
public void setVersionsDeleted(Integer versionsDeleted)
|
||||
{
|
||||
this.versionsDeleted = versionsDeleted;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for versionsDeleted.
|
||||
*******************************************************************************/
|
||||
public DeleteResult withVersionsDeleted(Integer versionsDeleted)
|
||||
{
|
||||
this.versionsDeleted = versionsDeleted;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for detail.
|
||||
*******************************************************************************/
|
||||
public String getDetail()
|
||||
{
|
||||
return (this.detail);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for detail.
|
||||
*******************************************************************************/
|
||||
public void setDetail(String detail)
|
||||
{
|
||||
this.detail = detail;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for detail.
|
||||
*******************************************************************************/
|
||||
public DeleteResult withDetail(String detail)
|
||||
{
|
||||
this.detail = detail;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
import com.kingsrook.qbits.objectstorage.internal.Db;
|
||||
import com.kingsrook.qbits.objectstorage.internal.ObjectStorageRuntime;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A reference provider that needs no code: it reads a table field that holds an
|
||||
** object key, a key prefix (for example a set of renditions), or a public URL
|
||||
** (parsed back to bucket and key with the PublicUrlResolver).
|
||||
**
|
||||
** <pre>
|
||||
** .withReferenceProvider(FieldReferenceProvider.keyPrefix("files", "media", "storageKey"))
|
||||
** </pre>
|
||||
*******************************************************************************/
|
||||
public class FieldReferenceProvider implements StorageReferenceProvider
|
||||
{
|
||||
/*******************************************************************************
|
||||
** What the field holds.
|
||||
*******************************************************************************/
|
||||
private enum Holds
|
||||
{
|
||||
KEY,
|
||||
KEY_PREFIX,
|
||||
KEY_FOLDER,
|
||||
URL
|
||||
}
|
||||
|
||||
private final String bucketAlias;
|
||||
private final String tableName;
|
||||
private final String fieldName;
|
||||
private final Holds holds;
|
||||
private String labelField;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor.
|
||||
*******************************************************************************/
|
||||
private FieldReferenceProvider(String bucketAlias, String tableName, String fieldName, Holds holds)
|
||||
{
|
||||
this.bucketAlias = bucketAlias;
|
||||
this.tableName = tableName;
|
||||
this.fieldName = fieldName;
|
||||
this.holds = holds;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The field holds one object key.
|
||||
*******************************************************************************/
|
||||
public static FieldReferenceProvider key(String bucketAlias, String tableName, String fieldName)
|
||||
{
|
||||
return (new FieldReferenceProvider(bucketAlias, tableName, fieldName, Holds.KEY));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The field holds a key prefix: every object under it is referenced.
|
||||
*******************************************************************************/
|
||||
public static FieldReferenceProvider keyPrefix(String bucketAlias, String tableName, String fieldName)
|
||||
{
|
||||
return (new FieldReferenceProvider(bucketAlias, tableName, fieldName, Holds.KEY_PREFIX));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The field holds the key of one object of a set that lives together in a
|
||||
** folder (an original and its renditions): every object in that key's folder
|
||||
** is referenced.
|
||||
*******************************************************************************/
|
||||
public static FieldReferenceProvider keyFolder(String bucketAlias, String tableName, String fieldName)
|
||||
{
|
||||
return (new FieldReferenceProvider(bucketAlias, tableName, fieldName, Holds.KEY_FOLDER));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The field holds a public URL.
|
||||
*******************************************************************************/
|
||||
public static FieldReferenceProvider url(String bucketAlias, String tableName, String fieldName)
|
||||
{
|
||||
return (new FieldReferenceProvider(bucketAlias, tableName, fieldName, Holds.URL));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Use another field as the label of the referencing record (default: the table
|
||||
** name and id).
|
||||
*******************************************************************************/
|
||||
public FieldReferenceProvider withLabelField(String labelField)
|
||||
{
|
||||
this.labelField = labelField;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Every reference this provider knows of in a bucket.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Stream<StorageReference> references(String alias) throws QException
|
||||
{
|
||||
if(!bucketAlias.equals(alias) && holds != Holds.URL)
|
||||
{
|
||||
return (Stream.empty());
|
||||
}
|
||||
List<StorageReference> references = new ArrayList<>();
|
||||
QQueryFilter filter = new QQueryFilter(new QFilterCriteria(fieldName, QCriteriaOperator.IS_NOT_BLANK));
|
||||
Db.forEachPage(tableName, filter, 2000, rows ->
|
||||
{
|
||||
for(QRecord row : rows)
|
||||
{
|
||||
String value = row.getValueString(fieldName);
|
||||
if(!StringUtils.hasContent(value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String label = labelField != null && row.getValueString(labelField) != null ? row.getValueString(labelField) : tableName + " #" + row.getValue("id");
|
||||
if(holds == Holds.URL)
|
||||
{
|
||||
Optional<BucketKey> parsed = ObjectStorageRuntime.publicUrlResolver().parse(value);
|
||||
if(parsed.isPresent() && parsed.get().getBucketAlias().equals(alias))
|
||||
{
|
||||
references.add(new StorageReference().withBucketAlias(alias).withKeyOrPrefix(parsed.get().getKey()).withMatchMode(MatchMode.KEY).withTableName(tableName)
|
||||
.withRecordId(String.valueOf(row.getValue("id"))).withFieldName(fieldName).withLabel(label));
|
||||
}
|
||||
}
|
||||
else if(holds == Holds.KEY_FOLDER)
|
||||
{
|
||||
int slash = value.lastIndexOf('/');
|
||||
if(slash > 0)
|
||||
{
|
||||
references.add(new StorageReference().withBucketAlias(alias).withKeyOrPrefix(StorageKeys.normalizePrefix(value.substring(0, slash + 1))).withMatchMode(MatchMode.PREFIX)
|
||||
.withTableName(tableName).withRecordId(String.valueOf(row.getValue("id"))).withFieldName(fieldName).withLabel(label));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
references.add(new StorageReference().withBucketAlias(alias).withKeyOrPrefix(StorageKeys.normalizePrefix(value)).withMatchMode(holds == Holds.KEY_PREFIX ? MatchMode.PREFIX : MatchMode.KEY)
|
||||
.withTableName(tableName).withRecordId(String.valueOf(row.getValue("id"))).withFieldName(fieldName).withLabel(label));
|
||||
}
|
||||
}
|
||||
});
|
||||
return (references.stream());
|
||||
}
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** A listing request: prefix, optional delimiter, page size and continuation token.
|
||||
*******************************************************************************/
|
||||
public class ListRequest
|
||||
{
|
||||
private String prefix = "";
|
||||
private String delimiter;
|
||||
private Integer pageSize = 1000;
|
||||
private String after;
|
||||
private String startAfter;
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Default constructor.
|
||||
*******************************************************************************/
|
||||
public ListRequest()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for prefix.
|
||||
*******************************************************************************/
|
||||
public String getPrefix()
|
||||
{
|
||||
return (this.prefix);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for prefix.
|
||||
*******************************************************************************/
|
||||
public void setPrefix(String prefix)
|
||||
{
|
||||
this.prefix = prefix;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for prefix.
|
||||
*******************************************************************************/
|
||||
public ListRequest withPrefix(String prefix)
|
||||
{
|
||||
this.prefix = prefix;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for delimiter.
|
||||
*******************************************************************************/
|
||||
public String getDelimiter()
|
||||
{
|
||||
return (this.delimiter);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for delimiter.
|
||||
*******************************************************************************/
|
||||
public void setDelimiter(String delimiter)
|
||||
{
|
||||
this.delimiter = delimiter;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for delimiter.
|
||||
*******************************************************************************/
|
||||
public ListRequest withDelimiter(String delimiter)
|
||||
{
|
||||
this.delimiter = delimiter;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for pageSize.
|
||||
*******************************************************************************/
|
||||
public Integer getPageSize()
|
||||
{
|
||||
return (this.pageSize);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for pageSize.
|
||||
*******************************************************************************/
|
||||
public void setPageSize(Integer pageSize)
|
||||
{
|
||||
this.pageSize = pageSize;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for pageSize.
|
||||
*******************************************************************************/
|
||||
public ListRequest withPageSize(Integer pageSize)
|
||||
{
|
||||
this.pageSize = pageSize;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for after.
|
||||
*******************************************************************************/
|
||||
public String getAfter()
|
||||
{
|
||||
return (this.after);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for after.
|
||||
*******************************************************************************/
|
||||
public void setAfter(String after)
|
||||
{
|
||||
this.after = after;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for after.
|
||||
*******************************************************************************/
|
||||
public ListRequest withAfter(String after)
|
||||
{
|
||||
this.after = after;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for startAfter.
|
||||
*******************************************************************************/
|
||||
public String getStartAfter()
|
||||
{
|
||||
return (this.startAfter);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for startAfter.
|
||||
*******************************************************************************/
|
||||
public void setStartAfter(String startAfter)
|
||||
{
|
||||
this.startAfter = startAfter;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for startAfter.
|
||||
*******************************************************************************/
|
||||
public ListRequest withStartAfter(String startAfter)
|
||||
{
|
||||
this.startAfter = startAfter;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A request for a prefix.
|
||||
*******************************************************************************/
|
||||
public static ListRequest prefix(String prefix)
|
||||
{
|
||||
return (new ListRequest().withPrefix(prefix));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Short-form fluent setter for delimiter.
|
||||
*******************************************************************************/
|
||||
public ListRequest delimiter(String delimiter)
|
||||
{
|
||||
this.delimiter = delimiter;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Short-form fluent setter for pageSize.
|
||||
*******************************************************************************/
|
||||
public ListRequest pageSize(Integer pageSize)
|
||||
{
|
||||
this.pageSize = pageSize;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Short-form fluent setter for after.
|
||||
*******************************************************************************/
|
||||
public ListRequest after(String after)
|
||||
{
|
||||
this.after = after;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Short-form fluent setter for startAfter.
|
||||
*******************************************************************************/
|
||||
public ListRequest startAfter(String startAfter)
|
||||
{
|
||||
this.startAfter = startAfter;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** S3 object lock retention mode.
|
||||
*******************************************************************************/
|
||||
public enum LockMode
|
||||
{
|
||||
GOVERNANCE,
|
||||
COMPLIANCE
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Whether a reference points at one key or at every key under a prefix.
|
||||
*******************************************************************************/
|
||||
public enum MatchMode
|
||||
{
|
||||
KEY,
|
||||
PREFIX
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Whether a copy keeps or replaces the source metadata.
|
||||
*******************************************************************************/
|
||||
public enum MetadataDirective
|
||||
{
|
||||
COPY,
|
||||
REPLACE
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
import com.kingsrook.qbits.objectstorage.config.StorageBucketConfig;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Decides the headers for a new or rewritten object. The default
|
||||
** implementation reads the object_storage_header_rule table; an app can plug
|
||||
** in its own (for example one driven by per-media-type settings it keeps).
|
||||
*******************************************************************************/
|
||||
public interface ObjectHeaderPolicy
|
||||
{
|
||||
/*******************************************************************************
|
||||
** The headers (Cache-Control, Content-Disposition) the object should have, or
|
||||
** empty headers when the policy has no opinion.
|
||||
*******************************************************************************/
|
||||
ObjectHeaders headersFor(StorageBucketConfig bucket, String key, String contentType);
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** The HTTP headers and user metadata stored with an object.
|
||||
*******************************************************************************/
|
||||
public class ObjectHeaders
|
||||
{
|
||||
private String cacheControl;
|
||||
private String contentType;
|
||||
private String contentDisposition;
|
||||
private String contentEncoding;
|
||||
private Map<String, String> userMetadata;
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Default constructor.
|
||||
*******************************************************************************/
|
||||
public ObjectHeaders()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for cacheControl.
|
||||
*******************************************************************************/
|
||||
public String getCacheControl()
|
||||
{
|
||||
return (this.cacheControl);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for cacheControl.
|
||||
*******************************************************************************/
|
||||
public void setCacheControl(String cacheControl)
|
||||
{
|
||||
this.cacheControl = cacheControl;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for cacheControl.
|
||||
*******************************************************************************/
|
||||
public ObjectHeaders withCacheControl(String cacheControl)
|
||||
{
|
||||
this.cacheControl = cacheControl;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for contentType.
|
||||
*******************************************************************************/
|
||||
public String getContentType()
|
||||
{
|
||||
return (this.contentType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for contentType.
|
||||
*******************************************************************************/
|
||||
public void setContentType(String contentType)
|
||||
{
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for contentType.
|
||||
*******************************************************************************/
|
||||
public ObjectHeaders withContentType(String contentType)
|
||||
{
|
||||
this.contentType = contentType;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for contentDisposition.
|
||||
*******************************************************************************/
|
||||
public String getContentDisposition()
|
||||
{
|
||||
return (this.contentDisposition);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for contentDisposition.
|
||||
*******************************************************************************/
|
||||
public void setContentDisposition(String contentDisposition)
|
||||
{
|
||||
this.contentDisposition = contentDisposition;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for contentDisposition.
|
||||
*******************************************************************************/
|
||||
public ObjectHeaders withContentDisposition(String contentDisposition)
|
||||
{
|
||||
this.contentDisposition = contentDisposition;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for contentEncoding.
|
||||
*******************************************************************************/
|
||||
public String getContentEncoding()
|
||||
{
|
||||
return (this.contentEncoding);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for contentEncoding.
|
||||
*******************************************************************************/
|
||||
public void setContentEncoding(String contentEncoding)
|
||||
{
|
||||
this.contentEncoding = contentEncoding;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for contentEncoding.
|
||||
*******************************************************************************/
|
||||
public ObjectHeaders withContentEncoding(String contentEncoding)
|
||||
{
|
||||
this.contentEncoding = contentEncoding;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for userMetadata.
|
||||
*******************************************************************************/
|
||||
public Map<String, String> getUserMetadata()
|
||||
{
|
||||
return (this.userMetadata);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for userMetadata.
|
||||
*******************************************************************************/
|
||||
public void setUserMetadata(Map<String, String> userMetadata)
|
||||
{
|
||||
this.userMetadata = userMetadata;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for userMetadata.
|
||||
*******************************************************************************/
|
||||
public ObjectHeaders withUserMetadata(Map<String, String> userMetadata)
|
||||
{
|
||||
this.userMetadata = userMetadata;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Headers with only a Cache-Control.
|
||||
*******************************************************************************/
|
||||
public static ObjectHeaders cacheControl(String cacheControl)
|
||||
{
|
||||
return (new ObjectHeaders().withCacheControl(cacheControl));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether no header or metadata is set.
|
||||
*******************************************************************************/
|
||||
public boolean isEmpty()
|
||||
{
|
||||
return (cacheControl == null && contentType == null && contentDisposition == null && contentEncoding == null && (userMetadata == null || userMetadata.isEmpty()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Equality on the headers that matter for policy checks (Cache-Control and
|
||||
** Content-Disposition; null and blank are the same).
|
||||
*******************************************************************************/
|
||||
public boolean policyEquals(ObjectHeaders other)
|
||||
{
|
||||
return (other != null
|
||||
&& Objects.equals(blankToNull(cacheControl), blankToNull(other.cacheControl))
|
||||
&& Objects.equals(blankToNull(contentDisposition), blankToNull(other.contentDisposition)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Blank to null.
|
||||
*******************************************************************************/
|
||||
private static String blankToNull(String s)
|
||||
{
|
||||
return (s == null || s.isBlank() ? null : s.trim());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
import com.kingsrook.qbits.objectstorage.internal.ConfigSync;
|
||||
import com.kingsrook.qbits.objectstorage.internal.ObjectStorageRuntime;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Entry point of the storage API. Application code and other qbits call
|
||||
** ObjectStorage.bucket("alias") instead of building their own S3 client.
|
||||
**
|
||||
** <pre>
|
||||
** StorageBucket files = ObjectStorage.bucket("files");
|
||||
** files.put(PutRequest.of("media/42/hero.jpg", bytes).withContentType("image/jpeg"));
|
||||
** URL link = files.presignGet(PresignGet.of("media/42/hero.jpg"));
|
||||
**
|
||||
** // a calling qbit with its own permission check (for example backups):
|
||||
** ObjectStorage.asSystem("backups", runId).bucket("backups").put(...);
|
||||
** </pre>
|
||||
*******************************************************************************/
|
||||
public final class ObjectStorage
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private ObjectStorage()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Install a configuration without a QQQ instance, for command-line tools and
|
||||
** tests that use the service but not the tables or the app. Connections and
|
||||
** buckets work as usual; nothing is written to the object_storage_* tables
|
||||
** (there are none in this mode), so writes are logged to the application log
|
||||
** only. Inside a QQQ application, produce the qbit instead.
|
||||
*******************************************************************************/
|
||||
public static void configureStandalone(com.kingsrook.qbits.objectstorage.ObjectStorageQBitConfig config)
|
||||
{
|
||||
java.util.List<String> errors = new java.util.ArrayList<>();
|
||||
config.validate(null, errors);
|
||||
if(!errors.isEmpty())
|
||||
{
|
||||
throw (new IllegalArgumentException("Object storage configuration is invalid: " + String.join("; ", errors)));
|
||||
}
|
||||
ObjectStorageRuntime.install(config);
|
||||
ConfigSync.reset();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The bucket behind an alias from config, called as the current session user.
|
||||
*******************************************************************************/
|
||||
public static StorageBucket bucket(String alias)
|
||||
{
|
||||
ConfigSync.ensureSynced();
|
||||
return (new StorageBucket(alias, StorageCaller.user()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A facade whose calls run as a system caller: tagged with the calling qbit
|
||||
** and its run, not subject to the user's object_storage.* permissions (the
|
||||
** calling qbit must have checked its own permission first), and logged as
|
||||
** "system: backups run 1234 (started by user X)".
|
||||
*******************************************************************************/
|
||||
public static SystemStorage asSystem(String callerName, String runId)
|
||||
{
|
||||
return (new SystemStorage(StorageCaller.system(callerName, runId)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Register a change listener (the CDN qbit plans purges with one).
|
||||
*******************************************************************************/
|
||||
public static void addChangeListener(StorageChangeListener listener)
|
||||
{
|
||||
ObjectStorageRuntime.addChangeListener(listener);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Replace the public URL resolver (the CDN qbit returns its hostnames).
|
||||
*******************************************************************************/
|
||||
public static void setPublicUrlResolver(PublicUrlResolver resolver)
|
||||
{
|
||||
ObjectStorageRuntime.setPublicUrlResolver(resolver);
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.time.Instant;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The content of an object plus its basic metadata. Close it when done.
|
||||
*******************************************************************************/
|
||||
public class ObjectStream implements AutoCloseable
|
||||
{
|
||||
private final InputStream inputStream;
|
||||
private final Long contentLength;
|
||||
private final String contentType;
|
||||
private final String etag;
|
||||
private final Instant lastModified;
|
||||
private final ObjectHeaders headers;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor.
|
||||
*******************************************************************************/
|
||||
public ObjectStream(InputStream inputStream, Long contentLength, String contentType, String etag, Instant lastModified, ObjectHeaders headers)
|
||||
{
|
||||
this.inputStream = inputStream;
|
||||
this.contentLength = contentLength;
|
||||
this.contentType = contentType;
|
||||
this.etag = etag;
|
||||
this.lastModified = lastModified;
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The content.
|
||||
*******************************************************************************/
|
||||
public InputStream getInputStream()
|
||||
{
|
||||
return (inputStream);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Read everything into memory (small objects only).
|
||||
*******************************************************************************/
|
||||
public byte[] readAllBytes() throws IOException
|
||||
{
|
||||
return (inputStream.readAllBytes());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for contentLength.
|
||||
*******************************************************************************/
|
||||
public Long getContentLength()
|
||||
{
|
||||
return (contentLength);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for contentType.
|
||||
*******************************************************************************/
|
||||
public String getContentType()
|
||||
{
|
||||
return (contentType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for etag.
|
||||
*******************************************************************************/
|
||||
public String getEtag()
|
||||
{
|
||||
return (etag);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for lastModified.
|
||||
*******************************************************************************/
|
||||
public Instant getLastModified()
|
||||
{
|
||||
return (lastModified);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for headers.
|
||||
*******************************************************************************/
|
||||
public ObjectHeaders getHeaders()
|
||||
{
|
||||
return (headers);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Close the stream.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void close() throws IOException
|
||||
{
|
||||
inputStream.close();
|
||||
}
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** One object as reported by a listing (no headers).
|
||||
*******************************************************************************/
|
||||
public class ObjectSummary
|
||||
{
|
||||
private String key;
|
||||
private Long size;
|
||||
private String etag;
|
||||
private Instant lastModified;
|
||||
private String storageClass;
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Default constructor.
|
||||
*******************************************************************************/
|
||||
public ObjectSummary()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for key.
|
||||
*******************************************************************************/
|
||||
public String getKey()
|
||||
{
|
||||
return (this.key);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for key.
|
||||
*******************************************************************************/
|
||||
public void setKey(String key)
|
||||
{
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for key.
|
||||
*******************************************************************************/
|
||||
public ObjectSummary withKey(String key)
|
||||
{
|
||||
this.key = key;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for size.
|
||||
*******************************************************************************/
|
||||
public Long getSize()
|
||||
{
|
||||
return (this.size);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for size.
|
||||
*******************************************************************************/
|
||||
public void setSize(Long size)
|
||||
{
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for size.
|
||||
*******************************************************************************/
|
||||
public ObjectSummary withSize(Long size)
|
||||
{
|
||||
this.size = size;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for etag.
|
||||
*******************************************************************************/
|
||||
public String getEtag()
|
||||
{
|
||||
return (this.etag);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for etag.
|
||||
*******************************************************************************/
|
||||
public void setEtag(String etag)
|
||||
{
|
||||
this.etag = etag;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for etag.
|
||||
*******************************************************************************/
|
||||
public ObjectSummary withEtag(String etag)
|
||||
{
|
||||
this.etag = etag;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for lastModified.
|
||||
*******************************************************************************/
|
||||
public Instant getLastModified()
|
||||
{
|
||||
return (this.lastModified);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for lastModified.
|
||||
*******************************************************************************/
|
||||
public void setLastModified(Instant lastModified)
|
||||
{
|
||||
this.lastModified = lastModified;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for lastModified.
|
||||
*******************************************************************************/
|
||||
public ObjectSummary withLastModified(Instant lastModified)
|
||||
{
|
||||
this.lastModified = lastModified;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for storageClass.
|
||||
*******************************************************************************/
|
||||
public String getStorageClass()
|
||||
{
|
||||
return (this.storageClass);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for storageClass.
|
||||
*******************************************************************************/
|
||||
public void setStorageClass(String storageClass)
|
||||
{
|
||||
this.storageClass = storageClass;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for storageClass.
|
||||
*******************************************************************************/
|
||||
public ObjectSummary withStorageClass(String storageClass)
|
||||
{
|
||||
this.storageClass = storageClass;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** One page of results plus a continuation token (null at the end) and, for delimiter listings, the common prefixes.
|
||||
*******************************************************************************/
|
||||
public class Page<T>
|
||||
{
|
||||
private List<T> items = new ArrayList<>();
|
||||
private List<String> prefixes = new ArrayList<>();
|
||||
private String nextToken;
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Default constructor.
|
||||
*******************************************************************************/
|
||||
public Page()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for items.
|
||||
*******************************************************************************/
|
||||
public List<T> getItems()
|
||||
{
|
||||
return (this.items);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for items.
|
||||
*******************************************************************************/
|
||||
public void setItems(List<T> items)
|
||||
{
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for items.
|
||||
*******************************************************************************/
|
||||
public Page<T> withItems(List<T> items)
|
||||
{
|
||||
this.items = items;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for prefixes.
|
||||
*******************************************************************************/
|
||||
public List<String> getPrefixes()
|
||||
{
|
||||
return (this.prefixes);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for prefixes.
|
||||
*******************************************************************************/
|
||||
public void setPrefixes(List<String> prefixes)
|
||||
{
|
||||
this.prefixes = prefixes;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for prefixes.
|
||||
*******************************************************************************/
|
||||
public Page<T> withPrefixes(List<String> prefixes)
|
||||
{
|
||||
this.prefixes = prefixes;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for nextToken.
|
||||
*******************************************************************************/
|
||||
public String getNextToken()
|
||||
{
|
||||
return (this.nextToken);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for nextToken.
|
||||
*******************************************************************************/
|
||||
public void setNextToken(String nextToken)
|
||||
{
|
||||
this.nextToken = nextToken;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for nextToken.
|
||||
*******************************************************************************/
|
||||
public Page<T> withNextToken(String nextToken)
|
||||
{
|
||||
this.nextToken = nextToken;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** A presigned download link request.
|
||||
*******************************************************************************/
|
||||
public class PresignGet
|
||||
{
|
||||
private String key;
|
||||
private Duration ttl;
|
||||
private String downloadAs;
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Default constructor.
|
||||
*******************************************************************************/
|
||||
public PresignGet()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for key.
|
||||
*******************************************************************************/
|
||||
public String getKey()
|
||||
{
|
||||
return (this.key);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for key.
|
||||
*******************************************************************************/
|
||||
public void setKey(String key)
|
||||
{
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for key.
|
||||
*******************************************************************************/
|
||||
public PresignGet withKey(String key)
|
||||
{
|
||||
this.key = key;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for ttl.
|
||||
*******************************************************************************/
|
||||
public Duration getTtl()
|
||||
{
|
||||
return (this.ttl);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for ttl.
|
||||
*******************************************************************************/
|
||||
public void setTtl(Duration ttl)
|
||||
{
|
||||
this.ttl = ttl;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for ttl.
|
||||
*******************************************************************************/
|
||||
public PresignGet withTtl(Duration ttl)
|
||||
{
|
||||
this.ttl = ttl;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for downloadAs.
|
||||
*******************************************************************************/
|
||||
public String getDownloadAs()
|
||||
{
|
||||
return (this.downloadAs);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for downloadAs.
|
||||
*******************************************************************************/
|
||||
public void setDownloadAs(String downloadAs)
|
||||
{
|
||||
this.downloadAs = downloadAs;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for downloadAs.
|
||||
*******************************************************************************/
|
||||
public PresignGet withDownloadAs(String downloadAs)
|
||||
{
|
||||
this.downloadAs = downloadAs;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A presigned GET for a key.
|
||||
*******************************************************************************/
|
||||
public static PresignGet of(String key)
|
||||
{
|
||||
return (new PresignGet().withKey(key));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Short-form fluent setter for ttl.
|
||||
*******************************************************************************/
|
||||
public PresignGet ttl(Duration ttl)
|
||||
{
|
||||
this.ttl = ttl;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Short-form fluent setter for downloadAs.
|
||||
*******************************************************************************/
|
||||
public PresignGet downloadAs(String downloadAs)
|
||||
{
|
||||
this.downloadAs = downloadAs;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** A presigned POST policy request (browser form upload with a size range).
|
||||
*******************************************************************************/
|
||||
public class PresignPost
|
||||
{
|
||||
private String key;
|
||||
private Duration ttl;
|
||||
private String contentType;
|
||||
private Long minBytes = 0L;
|
||||
private Long maxBytes;
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Default constructor.
|
||||
*******************************************************************************/
|
||||
public PresignPost()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for key.
|
||||
*******************************************************************************/
|
||||
public String getKey()
|
||||
{
|
||||
return (this.key);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for key.
|
||||
*******************************************************************************/
|
||||
public void setKey(String key)
|
||||
{
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for key.
|
||||
*******************************************************************************/
|
||||
public PresignPost withKey(String key)
|
||||
{
|
||||
this.key = key;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for ttl.
|
||||
*******************************************************************************/
|
||||
public Duration getTtl()
|
||||
{
|
||||
return (this.ttl);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for ttl.
|
||||
*******************************************************************************/
|
||||
public void setTtl(Duration ttl)
|
||||
{
|
||||
this.ttl = ttl;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for ttl.
|
||||
*******************************************************************************/
|
||||
public PresignPost withTtl(Duration ttl)
|
||||
{
|
||||
this.ttl = ttl;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for contentType.
|
||||
*******************************************************************************/
|
||||
public String getContentType()
|
||||
{
|
||||
return (this.contentType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for contentType.
|
||||
*******************************************************************************/
|
||||
public void setContentType(String contentType)
|
||||
{
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for contentType.
|
||||
*******************************************************************************/
|
||||
public PresignPost withContentType(String contentType)
|
||||
{
|
||||
this.contentType = contentType;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for minBytes.
|
||||
*******************************************************************************/
|
||||
public Long getMinBytes()
|
||||
{
|
||||
return (this.minBytes);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for minBytes.
|
||||
*******************************************************************************/
|
||||
public void setMinBytes(Long minBytes)
|
||||
{
|
||||
this.minBytes = minBytes;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for minBytes.
|
||||
*******************************************************************************/
|
||||
public PresignPost withMinBytes(Long minBytes)
|
||||
{
|
||||
this.minBytes = minBytes;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for maxBytes.
|
||||
*******************************************************************************/
|
||||
public Long getMaxBytes()
|
||||
{
|
||||
return (this.maxBytes);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for maxBytes.
|
||||
*******************************************************************************/
|
||||
public void setMaxBytes(Long maxBytes)
|
||||
{
|
||||
this.maxBytes = maxBytes;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for maxBytes.
|
||||
*******************************************************************************/
|
||||
public PresignPost withMaxBytes(Long maxBytes)
|
||||
{
|
||||
this.maxBytes = maxBytes;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** A presigned upload link request; the content type and length are signed.
|
||||
*******************************************************************************/
|
||||
public class PresignPut
|
||||
{
|
||||
private String key;
|
||||
private Duration ttl;
|
||||
private String contentType;
|
||||
private Long contentLength;
|
||||
private String cacheControl;
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Default constructor.
|
||||
*******************************************************************************/
|
||||
public PresignPut()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for key.
|
||||
*******************************************************************************/
|
||||
public String getKey()
|
||||
{
|
||||
return (this.key);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for key.
|
||||
*******************************************************************************/
|
||||
public void setKey(String key)
|
||||
{
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for key.
|
||||
*******************************************************************************/
|
||||
public PresignPut withKey(String key)
|
||||
{
|
||||
this.key = key;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for ttl.
|
||||
*******************************************************************************/
|
||||
public Duration getTtl()
|
||||
{
|
||||
return (this.ttl);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for ttl.
|
||||
*******************************************************************************/
|
||||
public void setTtl(Duration ttl)
|
||||
{
|
||||
this.ttl = ttl;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for ttl.
|
||||
*******************************************************************************/
|
||||
public PresignPut withTtl(Duration ttl)
|
||||
{
|
||||
this.ttl = ttl;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for contentType.
|
||||
*******************************************************************************/
|
||||
public String getContentType()
|
||||
{
|
||||
return (this.contentType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for contentType.
|
||||
*******************************************************************************/
|
||||
public void setContentType(String contentType)
|
||||
{
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for contentType.
|
||||
*******************************************************************************/
|
||||
public PresignPut withContentType(String contentType)
|
||||
{
|
||||
this.contentType = contentType;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for contentLength.
|
||||
*******************************************************************************/
|
||||
public Long getContentLength()
|
||||
{
|
||||
return (this.contentLength);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for contentLength.
|
||||
*******************************************************************************/
|
||||
public void setContentLength(Long contentLength)
|
||||
{
|
||||
this.contentLength = contentLength;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for contentLength.
|
||||
*******************************************************************************/
|
||||
public PresignPut withContentLength(Long contentLength)
|
||||
{
|
||||
this.contentLength = contentLength;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for cacheControl.
|
||||
*******************************************************************************/
|
||||
public String getCacheControl()
|
||||
{
|
||||
return (this.cacheControl);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for cacheControl.
|
||||
*******************************************************************************/
|
||||
public void setCacheControl(String cacheControl)
|
||||
{
|
||||
this.cacheControl = cacheControl;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for cacheControl.
|
||||
*******************************************************************************/
|
||||
public PresignPut withCacheControl(String cacheControl)
|
||||
{
|
||||
this.cacheControl = cacheControl;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A presigned PUT for a key.
|
||||
*******************************************************************************/
|
||||
public static PresignPut of(String key)
|
||||
{
|
||||
return (new PresignPut().withKey(key));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for the content type (short form).
|
||||
*******************************************************************************/
|
||||
public PresignPut contentType(String contentType)
|
||||
{
|
||||
this.contentType = contentType;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Short-form fluent setter for contentLength.
|
||||
*******************************************************************************/
|
||||
public PresignPut contentLength(Long contentLength)
|
||||
{
|
||||
this.contentLength = contentLength;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Short-form fluent setter for ttl.
|
||||
*******************************************************************************/
|
||||
public PresignPut ttl(Duration ttl)
|
||||
{
|
||||
this.ttl = ttl;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** The URL and form fields for a presigned POST.
|
||||
*******************************************************************************/
|
||||
public class PresignedPost
|
||||
{
|
||||
private String url;
|
||||
private Map<String, String> fields = new LinkedHashMap<>();
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Default constructor.
|
||||
*******************************************************************************/
|
||||
public PresignedPost()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for url.
|
||||
*******************************************************************************/
|
||||
public String getUrl()
|
||||
{
|
||||
return (this.url);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for url.
|
||||
*******************************************************************************/
|
||||
public void setUrl(String url)
|
||||
{
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for url.
|
||||
*******************************************************************************/
|
||||
public PresignedPost withUrl(String url)
|
||||
{
|
||||
this.url = url;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for fields.
|
||||
*******************************************************************************/
|
||||
public Map<String, String> getFields()
|
||||
{
|
||||
return (this.fields);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for fields.
|
||||
*******************************************************************************/
|
||||
public void setFields(Map<String, String> fields)
|
||||
{
|
||||
this.fields = fields;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for fields.
|
||||
*******************************************************************************/
|
||||
public PresignedPost withFields(Map<String, String> fields)
|
||||
{
|
||||
this.fields = fields;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qbits.objectstorage.config.StorageBucketConfig;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Maps (bucket, key) to the URL the world uses, and back. The default is
|
||||
** publicBaseUrl + "/" + encodedKey; the CDN qbit supplies one that returns the
|
||||
** CDN hostname.
|
||||
*******************************************************************************/
|
||||
public interface PublicUrlResolver
|
||||
{
|
||||
/*******************************************************************************
|
||||
** The public URL of a key, or null when the bucket has no public URL.
|
||||
*******************************************************************************/
|
||||
String resolve(StorageBucketConfig bucket, String key);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The reverse mapping, used by reference providers that store URLs.
|
||||
*******************************************************************************/
|
||||
Optional<BucketKey> parse(String url);
|
||||
}
|
||||
@@ -1,460 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** A put of one object from bytes or a stream.
|
||||
*******************************************************************************/
|
||||
public class PutRequest
|
||||
{
|
||||
private String key;
|
||||
private byte[] bytes;
|
||||
private InputStream stream;
|
||||
private Long contentLength;
|
||||
private String contentType;
|
||||
private String cacheControl;
|
||||
private String contentDisposition;
|
||||
private Map<String, String> metadata;
|
||||
private LockMode lockMode;
|
||||
private Instant lockRetainUntil;
|
||||
private String storageClass;
|
||||
private Boolean ifNoneMatch = false;
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Default constructor.
|
||||
*******************************************************************************/
|
||||
public PutRequest()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for key.
|
||||
*******************************************************************************/
|
||||
public String getKey()
|
||||
{
|
||||
return (this.key);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for key.
|
||||
*******************************************************************************/
|
||||
public void setKey(String key)
|
||||
{
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for key.
|
||||
*******************************************************************************/
|
||||
public PutRequest withKey(String key)
|
||||
{
|
||||
this.key = key;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for bytes.
|
||||
*******************************************************************************/
|
||||
public byte[] getBytes()
|
||||
{
|
||||
return (this.bytes);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for bytes.
|
||||
*******************************************************************************/
|
||||
public void setBytes(byte[] bytes)
|
||||
{
|
||||
this.bytes = bytes;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for bytes.
|
||||
*******************************************************************************/
|
||||
public PutRequest withBytes(byte[] bytes)
|
||||
{
|
||||
this.bytes = bytes;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for stream.
|
||||
*******************************************************************************/
|
||||
public InputStream getStream()
|
||||
{
|
||||
return (this.stream);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for stream.
|
||||
*******************************************************************************/
|
||||
public void setStream(InputStream stream)
|
||||
{
|
||||
this.stream = stream;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for stream.
|
||||
*******************************************************************************/
|
||||
public PutRequest withStream(InputStream stream)
|
||||
{
|
||||
this.stream = stream;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for contentLength.
|
||||
*******************************************************************************/
|
||||
public Long getContentLength()
|
||||
{
|
||||
return (this.contentLength);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for contentLength.
|
||||
*******************************************************************************/
|
||||
public void setContentLength(Long contentLength)
|
||||
{
|
||||
this.contentLength = contentLength;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for contentLength.
|
||||
*******************************************************************************/
|
||||
public PutRequest withContentLength(Long contentLength)
|
||||
{
|
||||
this.contentLength = contentLength;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for contentType.
|
||||
*******************************************************************************/
|
||||
public String getContentType()
|
||||
{
|
||||
return (this.contentType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for contentType.
|
||||
*******************************************************************************/
|
||||
public void setContentType(String contentType)
|
||||
{
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for contentType.
|
||||
*******************************************************************************/
|
||||
public PutRequest withContentType(String contentType)
|
||||
{
|
||||
this.contentType = contentType;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for cacheControl.
|
||||
*******************************************************************************/
|
||||
public String getCacheControl()
|
||||
{
|
||||
return (this.cacheControl);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for cacheControl.
|
||||
*******************************************************************************/
|
||||
public void setCacheControl(String cacheControl)
|
||||
{
|
||||
this.cacheControl = cacheControl;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for cacheControl.
|
||||
*******************************************************************************/
|
||||
public PutRequest withCacheControl(String cacheControl)
|
||||
{
|
||||
this.cacheControl = cacheControl;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for contentDisposition.
|
||||
*******************************************************************************/
|
||||
public String getContentDisposition()
|
||||
{
|
||||
return (this.contentDisposition);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for contentDisposition.
|
||||
*******************************************************************************/
|
||||
public void setContentDisposition(String contentDisposition)
|
||||
{
|
||||
this.contentDisposition = contentDisposition;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for contentDisposition.
|
||||
*******************************************************************************/
|
||||
public PutRequest withContentDisposition(String contentDisposition)
|
||||
{
|
||||
this.contentDisposition = contentDisposition;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for metadata.
|
||||
*******************************************************************************/
|
||||
public Map<String, String> getMetadata()
|
||||
{
|
||||
return (this.metadata);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for metadata.
|
||||
*******************************************************************************/
|
||||
public void setMetadata(Map<String, String> metadata)
|
||||
{
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for metadata.
|
||||
*******************************************************************************/
|
||||
public PutRequest withMetadata(Map<String, String> metadata)
|
||||
{
|
||||
this.metadata = metadata;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for lockMode.
|
||||
*******************************************************************************/
|
||||
public LockMode getLockMode()
|
||||
{
|
||||
return (this.lockMode);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for lockMode.
|
||||
*******************************************************************************/
|
||||
public void setLockMode(LockMode lockMode)
|
||||
{
|
||||
this.lockMode = lockMode;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for lockMode.
|
||||
*******************************************************************************/
|
||||
public PutRequest withLockMode(LockMode lockMode)
|
||||
{
|
||||
this.lockMode = lockMode;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for lockRetainUntil.
|
||||
*******************************************************************************/
|
||||
public Instant getLockRetainUntil()
|
||||
{
|
||||
return (this.lockRetainUntil);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for lockRetainUntil.
|
||||
*******************************************************************************/
|
||||
public void setLockRetainUntil(Instant lockRetainUntil)
|
||||
{
|
||||
this.lockRetainUntil = lockRetainUntil;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for lockRetainUntil.
|
||||
*******************************************************************************/
|
||||
public PutRequest withLockRetainUntil(Instant lockRetainUntil)
|
||||
{
|
||||
this.lockRetainUntil = lockRetainUntil;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for storageClass.
|
||||
*******************************************************************************/
|
||||
public String getStorageClass()
|
||||
{
|
||||
return (this.storageClass);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for storageClass.
|
||||
*******************************************************************************/
|
||||
public void setStorageClass(String storageClass)
|
||||
{
|
||||
this.storageClass = storageClass;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for storageClass.
|
||||
*******************************************************************************/
|
||||
public PutRequest withStorageClass(String storageClass)
|
||||
{
|
||||
this.storageClass = storageClass;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for ifNoneMatch.
|
||||
*******************************************************************************/
|
||||
public Boolean getIfNoneMatch()
|
||||
{
|
||||
return (this.ifNoneMatch);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for ifNoneMatch.
|
||||
*******************************************************************************/
|
||||
public void setIfNoneMatch(Boolean ifNoneMatch)
|
||||
{
|
||||
this.ifNoneMatch = ifNoneMatch;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for ifNoneMatch.
|
||||
*******************************************************************************/
|
||||
public PutRequest withIfNoneMatch(Boolean ifNoneMatch)
|
||||
{
|
||||
this.ifNoneMatch = ifNoneMatch;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A put of bytes.
|
||||
*******************************************************************************/
|
||||
public static PutRequest of(String key, byte[] bytes)
|
||||
{
|
||||
return (new PutRequest().withKey(key).withBytes(bytes).withContentLength((long) bytes.length));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A put of a stream.
|
||||
*******************************************************************************/
|
||||
public static PutRequest of(String key, InputStream stream)
|
||||
{
|
||||
return (new PutRequest().withKey(key).withStream(stream));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Request object lock retention (offered only where the bucket supports it).
|
||||
*******************************************************************************/
|
||||
public PutRequest withObjectLock(LockMode mode, Instant retainUntil)
|
||||
{
|
||||
this.lockMode = mode;
|
||||
this.lockRetainUntil = retainUntil;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,263 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** The result of a put.
|
||||
*******************************************************************************/
|
||||
public class PutResult
|
||||
{
|
||||
private String key;
|
||||
private String etag;
|
||||
private String versionId;
|
||||
private Long bytes;
|
||||
private Boolean multipart = false;
|
||||
private String requestId;
|
||||
private ObjectHeaders appliedHeaders;
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Default constructor.
|
||||
*******************************************************************************/
|
||||
public PutResult()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for key.
|
||||
*******************************************************************************/
|
||||
public String getKey()
|
||||
{
|
||||
return (this.key);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for key.
|
||||
*******************************************************************************/
|
||||
public void setKey(String key)
|
||||
{
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for key.
|
||||
*******************************************************************************/
|
||||
public PutResult withKey(String key)
|
||||
{
|
||||
this.key = key;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for etag.
|
||||
*******************************************************************************/
|
||||
public String getEtag()
|
||||
{
|
||||
return (this.etag);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for etag.
|
||||
*******************************************************************************/
|
||||
public void setEtag(String etag)
|
||||
{
|
||||
this.etag = etag;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for etag.
|
||||
*******************************************************************************/
|
||||
public PutResult withEtag(String etag)
|
||||
{
|
||||
this.etag = etag;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for versionId.
|
||||
*******************************************************************************/
|
||||
public String getVersionId()
|
||||
{
|
||||
return (this.versionId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for versionId.
|
||||
*******************************************************************************/
|
||||
public void setVersionId(String versionId)
|
||||
{
|
||||
this.versionId = versionId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for versionId.
|
||||
*******************************************************************************/
|
||||
public PutResult withVersionId(String versionId)
|
||||
{
|
||||
this.versionId = versionId;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for bytes.
|
||||
*******************************************************************************/
|
||||
public Long getBytes()
|
||||
{
|
||||
return (this.bytes);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for bytes.
|
||||
*******************************************************************************/
|
||||
public void setBytes(Long bytes)
|
||||
{
|
||||
this.bytes = bytes;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for bytes.
|
||||
*******************************************************************************/
|
||||
public PutResult withBytes(Long bytes)
|
||||
{
|
||||
this.bytes = bytes;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for multipart.
|
||||
*******************************************************************************/
|
||||
public Boolean getMultipart()
|
||||
{
|
||||
return (this.multipart);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for multipart.
|
||||
*******************************************************************************/
|
||||
public void setMultipart(Boolean multipart)
|
||||
{
|
||||
this.multipart = multipart;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for multipart.
|
||||
*******************************************************************************/
|
||||
public PutResult withMultipart(Boolean multipart)
|
||||
{
|
||||
this.multipart = multipart;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for requestId.
|
||||
*******************************************************************************/
|
||||
public String getRequestId()
|
||||
{
|
||||
return (this.requestId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for requestId.
|
||||
*******************************************************************************/
|
||||
public void setRequestId(String requestId)
|
||||
{
|
||||
this.requestId = requestId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for requestId.
|
||||
*******************************************************************************/
|
||||
public PutResult withRequestId(String requestId)
|
||||
{
|
||||
this.requestId = requestId;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for appliedHeaders.
|
||||
*******************************************************************************/
|
||||
public ObjectHeaders getAppliedHeaders()
|
||||
{
|
||||
return (this.appliedHeaders);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for appliedHeaders.
|
||||
*******************************************************************************/
|
||||
public void setAppliedHeaders(ObjectHeaders appliedHeaders)
|
||||
{
|
||||
this.appliedHeaders = appliedHeaders;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for appliedHeaders.
|
||||
*******************************************************************************/
|
||||
public PutResult withAppliedHeaders(ObjectHeaders appliedHeaders)
|
||||
{
|
||||
this.appliedHeaders = appliedHeaders;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** A byte range for a get.
|
||||
*******************************************************************************/
|
||||
public class Range
|
||||
{
|
||||
private Long from;
|
||||
private Long to;
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Default constructor.
|
||||
*******************************************************************************/
|
||||
public Range()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for from.
|
||||
*******************************************************************************/
|
||||
public Long getFrom()
|
||||
{
|
||||
return (this.from);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for from.
|
||||
*******************************************************************************/
|
||||
public void setFrom(Long from)
|
||||
{
|
||||
this.from = from;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for from.
|
||||
*******************************************************************************/
|
||||
public Range withFrom(Long from)
|
||||
{
|
||||
this.from = from;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for to.
|
||||
*******************************************************************************/
|
||||
public Long getTo()
|
||||
{
|
||||
return (this.to);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for to.
|
||||
*******************************************************************************/
|
||||
public void setTo(Long to)
|
||||
{
|
||||
this.to = to;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for to.
|
||||
*******************************************************************************/
|
||||
public Range withTo(Long to)
|
||||
{
|
||||
this.to = to;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The whole object.
|
||||
*******************************************************************************/
|
||||
public static Range all()
|
||||
{
|
||||
return (new Range());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Bytes from..to inclusive.
|
||||
*******************************************************************************/
|
||||
public static Range bytes(long from, long to)
|
||||
{
|
||||
return (new Range().withFrom(from).withTo(to));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Where storage conditions are raised. The default is a no-op; when the Alerts
|
||||
** qbit is installed an adapter forwards to its producer SPI. Alerts carry a
|
||||
** stable dedupe key and counts, never secrets or presigned URLs.
|
||||
*******************************************************************************/
|
||||
public interface StorageAlertSink
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Raise (or refresh) an alert.
|
||||
*******************************************************************************/
|
||||
void raise(String alertType, String dedupeKey, String severity, String message, Map<String, Object> details);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Resolve an alert.
|
||||
*******************************************************************************/
|
||||
void resolve(String alertType, String dedupeKey);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A sink that does nothing.
|
||||
*******************************************************************************/
|
||||
static StorageAlertSink noop()
|
||||
{
|
||||
return (new StorageAlertSink()
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Does nothing.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void raise(String alertType, String dedupeKey, String severity, String message, Map<String, Object> details)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Does nothing.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void resolve(String alertType, String dedupeKey)
|
||||
{
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,134 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Who is making a storage call: the current session's user, or a system caller
|
||||
** (a calling qbit and its run, for example "backups" run 1234). System callers
|
||||
** are not subject to the user's object_storage.* permissions; the calling qbit
|
||||
** must have checked its own permission first. Every write is logged with this.
|
||||
*******************************************************************************/
|
||||
public final class StorageCaller
|
||||
{
|
||||
private final boolean system;
|
||||
private final String callerName;
|
||||
private final String runId;
|
||||
private final String startedBy;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor.
|
||||
*******************************************************************************/
|
||||
private StorageCaller(boolean system, String callerName, String runId, String startedBy)
|
||||
{
|
||||
this.system = system;
|
||||
this.callerName = callerName;
|
||||
this.runId = runId;
|
||||
this.startedBy = startedBy;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The current session's user as the caller.
|
||||
*******************************************************************************/
|
||||
public static StorageCaller user()
|
||||
{
|
||||
return (new StorageCaller(false, null, null, currentUserName()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A system caller: the calling qbit's name and its run id.
|
||||
*******************************************************************************/
|
||||
public static StorageCaller system(String callerName, String runId)
|
||||
{
|
||||
return (new StorageCaller(true, callerName, runId, currentUserName()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The name of the current user, or null.
|
||||
*******************************************************************************/
|
||||
private static String currentUserName()
|
||||
{
|
||||
QSession session = QContext.getQSession();
|
||||
if(session != null && session.getUser() != null)
|
||||
{
|
||||
String id = session.getUser().getIdReference();
|
||||
return (StringUtils.hasContent(id) ? id : session.getUser().getFullName());
|
||||
}
|
||||
return (null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether this is a system caller.
|
||||
*******************************************************************************/
|
||||
public boolean isSystem()
|
||||
{
|
||||
return (system);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for callerName.
|
||||
*******************************************************************************/
|
||||
public String getCallerName()
|
||||
{
|
||||
return (callerName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for runId.
|
||||
*******************************************************************************/
|
||||
public String getRunId()
|
||||
{
|
||||
return (runId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The text recorded in the operation log.
|
||||
*******************************************************************************/
|
||||
public String describe()
|
||||
{
|
||||
if(system)
|
||||
{
|
||||
return ("system: " + callerName + (runId == null ? "" : " run " + runId) + (startedBy == null ? "" : " (started by user " + startedBy + ")"));
|
||||
}
|
||||
return (startedBy == null ? "system (no user)" : startedBy);
|
||||
}
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** What a change listener is told after a successful write.
|
||||
*******************************************************************************/
|
||||
public class StorageChange
|
||||
{
|
||||
private String bucketAlias;
|
||||
private String key;
|
||||
private String destinationKey;
|
||||
private ChangeOperation operation;
|
||||
private String actor;
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Default constructor.
|
||||
*******************************************************************************/
|
||||
public StorageChange()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for bucketAlias.
|
||||
*******************************************************************************/
|
||||
public String getBucketAlias()
|
||||
{
|
||||
return (this.bucketAlias);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for bucketAlias.
|
||||
*******************************************************************************/
|
||||
public void setBucketAlias(String bucketAlias)
|
||||
{
|
||||
this.bucketAlias = bucketAlias;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for bucketAlias.
|
||||
*******************************************************************************/
|
||||
public StorageChange withBucketAlias(String bucketAlias)
|
||||
{
|
||||
this.bucketAlias = bucketAlias;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for key.
|
||||
*******************************************************************************/
|
||||
public String getKey()
|
||||
{
|
||||
return (this.key);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for key.
|
||||
*******************************************************************************/
|
||||
public void setKey(String key)
|
||||
{
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for key.
|
||||
*******************************************************************************/
|
||||
public StorageChange withKey(String key)
|
||||
{
|
||||
this.key = key;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for destinationKey.
|
||||
*******************************************************************************/
|
||||
public String getDestinationKey()
|
||||
{
|
||||
return (this.destinationKey);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for destinationKey.
|
||||
*******************************************************************************/
|
||||
public void setDestinationKey(String destinationKey)
|
||||
{
|
||||
this.destinationKey = destinationKey;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for destinationKey.
|
||||
*******************************************************************************/
|
||||
public StorageChange withDestinationKey(String destinationKey)
|
||||
{
|
||||
this.destinationKey = destinationKey;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for operation.
|
||||
*******************************************************************************/
|
||||
public ChangeOperation getOperation()
|
||||
{
|
||||
return (this.operation);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for operation.
|
||||
*******************************************************************************/
|
||||
public void setOperation(ChangeOperation operation)
|
||||
{
|
||||
this.operation = operation;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for operation.
|
||||
*******************************************************************************/
|
||||
public StorageChange withOperation(ChangeOperation operation)
|
||||
{
|
||||
this.operation = operation;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for actor.
|
||||
*******************************************************************************/
|
||||
public String getActor()
|
||||
{
|
||||
return (this.actor);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for actor.
|
||||
*******************************************************************************/
|
||||
public void setActor(String actor)
|
||||
{
|
||||
this.actor = actor;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for actor.
|
||||
*******************************************************************************/
|
||||
public StorageChange withActor(String actor)
|
||||
{
|
||||
this.actor = actor;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Called after every successful write (put, copy, header change, delete,
|
||||
** restore). The CDN qbit registers one to plan purges. Listeners run after the
|
||||
** write and never fail it; their errors are logged on the operation row.
|
||||
*******************************************************************************/
|
||||
public interface StorageChangeListener
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Called after a write.
|
||||
*******************************************************************************/
|
||||
void onChange(StorageChange change) throws Exception;
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The only exception the storage API throws. No raw SDK exception leaks out.
|
||||
** Carries a stable reason and the provider's request ID when there is one.
|
||||
*******************************************************************************/
|
||||
public class StorageException extends RuntimeException
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Stable failure reasons.
|
||||
*******************************************************************************/
|
||||
public enum Reason
|
||||
{
|
||||
NOT_FOUND,
|
||||
ACCESS_DENIED,
|
||||
UNSUPPORTED,
|
||||
PRECONDITION_FAILED,
|
||||
THROTTLED,
|
||||
PROVIDER_ERROR
|
||||
}
|
||||
|
||||
private final Reason reason;
|
||||
private final String requestId;
|
||||
private final Integer httpStatus;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor.
|
||||
*******************************************************************************/
|
||||
public StorageException(Reason reason, String message, String requestId, Integer httpStatus, Throwable cause)
|
||||
{
|
||||
super(message, cause);
|
||||
this.reason = reason;
|
||||
this.requestId = requestId;
|
||||
this.httpStatus = httpStatus;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor without a provider response.
|
||||
*******************************************************************************/
|
||||
public StorageException(Reason reason, String message)
|
||||
{
|
||||
this(reason, message, null, null, null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for reason.
|
||||
*******************************************************************************/
|
||||
public Reason getReason()
|
||||
{
|
||||
return (reason);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for requestId.
|
||||
*******************************************************************************/
|
||||
public String getRequestId()
|
||||
{
|
||||
return (requestId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for httpStatus.
|
||||
*******************************************************************************/
|
||||
public Integer getHttpStatus()
|
||||
{
|
||||
return (httpStatus);
|
||||
}
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HexFormat;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Builds and validates object keys, and percent-encodes them for URLs.
|
||||
** Apps keep their own key layouts; this only makes them consistent and safe.
|
||||
*******************************************************************************/
|
||||
public final class StorageKeys
|
||||
{
|
||||
public static final int MAX_KEY_BYTES = 1024;
|
||||
|
||||
private static final Pattern CONTROL_CHARS = Pattern.compile("[\\p{Cntrl}]");
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private StorageKeys()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Normalize and validate: collapse //, strip a leading /, reject .. segments,
|
||||
** control characters and keys over 1,024 bytes. Returns the normalized key.
|
||||
*******************************************************************************/
|
||||
public static String normalize(String key)
|
||||
{
|
||||
if(key == null || key.isEmpty())
|
||||
{
|
||||
throw (new IllegalArgumentException("An object key is required"));
|
||||
}
|
||||
if(CONTROL_CHARS.matcher(key).find())
|
||||
{
|
||||
throw (new IllegalArgumentException("An object key may not contain control characters"));
|
||||
}
|
||||
String normalized = key.replaceAll("/{2,}", "/");
|
||||
while(normalized.startsWith("/"))
|
||||
{
|
||||
normalized = normalized.substring(1);
|
||||
}
|
||||
if(normalized.isEmpty())
|
||||
{
|
||||
throw (new IllegalArgumentException("An object key is required"));
|
||||
}
|
||||
for(String segment : normalized.split("/", -1))
|
||||
{
|
||||
if("..".equals(segment))
|
||||
{
|
||||
throw (new IllegalArgumentException("An object key may not contain .. segments"));
|
||||
}
|
||||
}
|
||||
if(normalized.getBytes(StandardCharsets.UTF_8).length > MAX_KEY_BYTES)
|
||||
{
|
||||
throw (new IllegalArgumentException("An object key may not be longer than " + MAX_KEY_BYTES + " bytes"));
|
||||
}
|
||||
return (normalized);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Normalize a prefix (a key that may be empty or end with a slash).
|
||||
*******************************************************************************/
|
||||
public static String normalizePrefix(String prefix)
|
||||
{
|
||||
if(prefix == null || prefix.isEmpty() || "/".equals(prefix))
|
||||
{
|
||||
return ("");
|
||||
}
|
||||
boolean trailing = prefix.endsWith("/");
|
||||
String n = normalize(prefix);
|
||||
return (trailing && !n.endsWith("/") ? n + "/" : n);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A safe file name: no path separators or control characters, no leading dots.
|
||||
*******************************************************************************/
|
||||
public static String safeFilename(String name)
|
||||
{
|
||||
String n = name == null ? "" : name.replace('\\', '/');
|
||||
n = n.substring(n.lastIndexOf('/') + 1);
|
||||
n = CONTROL_CHARS.matcher(n).replaceAll("");
|
||||
n = n.replaceAll("^\\.+", "").trim();
|
||||
return (n.isEmpty() ? "file" : n);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The content-addressed layout {scope}/{slug}/{sha256}/{name}.
|
||||
*******************************************************************************/
|
||||
public static String contentAddressed(String scope, String slug, byte[] content, String name)
|
||||
{
|
||||
return (normalize(scope + "/" + slug + "/" + sha256Hex(content) + "/" + safeFilename(name)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Lowercase hex SHA-256.
|
||||
*******************************************************************************/
|
||||
public static String sha256Hex(byte[] content)
|
||||
{
|
||||
try
|
||||
{
|
||||
return (HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(content)));
|
||||
}
|
||||
catch(NoSuchAlgorithmException e)
|
||||
{
|
||||
throw (new IllegalStateException(e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Percent-encode a key for use in a URL path: slashes stay, and '+', space and
|
||||
** non-ASCII are encoded (the usual source of "works on MinIO, 403 on AWS").
|
||||
*******************************************************************************/
|
||||
public static String encodeForUrl(String key)
|
||||
{
|
||||
StringBuilder out = new StringBuilder();
|
||||
for(byte b : key.getBytes(StandardCharsets.UTF_8))
|
||||
{
|
||||
char c = (char) (b & 0xFF);
|
||||
if((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~' || c == '/')
|
||||
{
|
||||
out.append(c);
|
||||
}
|
||||
else
|
||||
{
|
||||
out.append('%').append(String.format("%02X", b & 0xFF));
|
||||
}
|
||||
}
|
||||
return (out.toString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The reverse of {@link #encodeForUrl(String)}.
|
||||
*******************************************************************************/
|
||||
public static String decodeFromUrl(String encoded)
|
||||
{
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
for(int i = 0; i < encoded.length(); i++)
|
||||
{
|
||||
char c = encoded.charAt(i);
|
||||
if(c == '%' && i + 2 < encoded.length())
|
||||
{
|
||||
out.write(Integer.parseInt(encoded.substring(i + 1, i + 3), 16));
|
||||
i += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
byte[] bytes = String.valueOf(c).getBytes(StandardCharsets.UTF_8);
|
||||
out.write(bytes, 0, bytes.length);
|
||||
}
|
||||
}
|
||||
return (out.toString(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
@@ -1,263 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** A record that points at a key or prefix in a bucket.
|
||||
*******************************************************************************/
|
||||
public class StorageReference
|
||||
{
|
||||
private String bucketAlias;
|
||||
private String keyOrPrefix;
|
||||
private MatchMode matchMode = MatchMode.KEY;
|
||||
private String tableName;
|
||||
private String recordId;
|
||||
private String fieldName;
|
||||
private String label;
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Default constructor.
|
||||
*******************************************************************************/
|
||||
public StorageReference()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for bucketAlias.
|
||||
*******************************************************************************/
|
||||
public String getBucketAlias()
|
||||
{
|
||||
return (this.bucketAlias);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for bucketAlias.
|
||||
*******************************************************************************/
|
||||
public void setBucketAlias(String bucketAlias)
|
||||
{
|
||||
this.bucketAlias = bucketAlias;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for bucketAlias.
|
||||
*******************************************************************************/
|
||||
public StorageReference withBucketAlias(String bucketAlias)
|
||||
{
|
||||
this.bucketAlias = bucketAlias;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for keyOrPrefix.
|
||||
*******************************************************************************/
|
||||
public String getKeyOrPrefix()
|
||||
{
|
||||
return (this.keyOrPrefix);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for keyOrPrefix.
|
||||
*******************************************************************************/
|
||||
public void setKeyOrPrefix(String keyOrPrefix)
|
||||
{
|
||||
this.keyOrPrefix = keyOrPrefix;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for keyOrPrefix.
|
||||
*******************************************************************************/
|
||||
public StorageReference withKeyOrPrefix(String keyOrPrefix)
|
||||
{
|
||||
this.keyOrPrefix = keyOrPrefix;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for matchMode.
|
||||
*******************************************************************************/
|
||||
public MatchMode getMatchMode()
|
||||
{
|
||||
return (this.matchMode);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for matchMode.
|
||||
*******************************************************************************/
|
||||
public void setMatchMode(MatchMode matchMode)
|
||||
{
|
||||
this.matchMode = matchMode;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for matchMode.
|
||||
*******************************************************************************/
|
||||
public StorageReference withMatchMode(MatchMode matchMode)
|
||||
{
|
||||
this.matchMode = matchMode;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for tableName.
|
||||
*******************************************************************************/
|
||||
public String getTableName()
|
||||
{
|
||||
return (this.tableName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for tableName.
|
||||
*******************************************************************************/
|
||||
public void setTableName(String tableName)
|
||||
{
|
||||
this.tableName = tableName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for tableName.
|
||||
*******************************************************************************/
|
||||
public StorageReference withTableName(String tableName)
|
||||
{
|
||||
this.tableName = tableName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for recordId.
|
||||
*******************************************************************************/
|
||||
public String getRecordId()
|
||||
{
|
||||
return (this.recordId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for recordId.
|
||||
*******************************************************************************/
|
||||
public void setRecordId(String recordId)
|
||||
{
|
||||
this.recordId = recordId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for recordId.
|
||||
*******************************************************************************/
|
||||
public StorageReference withRecordId(String recordId)
|
||||
{
|
||||
this.recordId = recordId;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for fieldName.
|
||||
*******************************************************************************/
|
||||
public String getFieldName()
|
||||
{
|
||||
return (this.fieldName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for fieldName.
|
||||
*******************************************************************************/
|
||||
public void setFieldName(String fieldName)
|
||||
{
|
||||
this.fieldName = fieldName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for fieldName.
|
||||
*******************************************************************************/
|
||||
public StorageReference withFieldName(String fieldName)
|
||||
{
|
||||
this.fieldName = fieldName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for label.
|
||||
*******************************************************************************/
|
||||
public String getLabel()
|
||||
{
|
||||
return (this.label);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for label.
|
||||
*******************************************************************************/
|
||||
public void setLabel(String label)
|
||||
{
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for label.
|
||||
*******************************************************************************/
|
||||
public StorageReference withLabel(String label)
|
||||
{
|
||||
this.label = label;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
import java.util.stream.Stream;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Lets an app declare which records point at storage. Most apps need no code:
|
||||
** see FieldReferenceProvider.
|
||||
*******************************************************************************/
|
||||
public interface StorageReferenceProvider
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Every reference this provider knows of in a bucket.
|
||||
*******************************************************************************/
|
||||
Stream<StorageReference> references(String bucketAlias) throws QException;
|
||||
}
|
||||
@@ -1,362 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** One object as reported by HEAD.
|
||||
*******************************************************************************/
|
||||
public class StoredObject
|
||||
{
|
||||
private String bucketAlias;
|
||||
private String key;
|
||||
private Long size;
|
||||
private String etag;
|
||||
private Instant lastModified;
|
||||
private ObjectHeaders headers = new ObjectHeaders();
|
||||
private String versionId;
|
||||
private String storageClass;
|
||||
private String lockMode;
|
||||
private Instant lockRetainUntil;
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Default constructor.
|
||||
*******************************************************************************/
|
||||
public StoredObject()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for bucketAlias.
|
||||
*******************************************************************************/
|
||||
public String getBucketAlias()
|
||||
{
|
||||
return (this.bucketAlias);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for bucketAlias.
|
||||
*******************************************************************************/
|
||||
public void setBucketAlias(String bucketAlias)
|
||||
{
|
||||
this.bucketAlias = bucketAlias;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for bucketAlias.
|
||||
*******************************************************************************/
|
||||
public StoredObject withBucketAlias(String bucketAlias)
|
||||
{
|
||||
this.bucketAlias = bucketAlias;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for key.
|
||||
*******************************************************************************/
|
||||
public String getKey()
|
||||
{
|
||||
return (this.key);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for key.
|
||||
*******************************************************************************/
|
||||
public void setKey(String key)
|
||||
{
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for key.
|
||||
*******************************************************************************/
|
||||
public StoredObject withKey(String key)
|
||||
{
|
||||
this.key = key;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for size.
|
||||
*******************************************************************************/
|
||||
public Long getSize()
|
||||
{
|
||||
return (this.size);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for size.
|
||||
*******************************************************************************/
|
||||
public void setSize(Long size)
|
||||
{
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for size.
|
||||
*******************************************************************************/
|
||||
public StoredObject withSize(Long size)
|
||||
{
|
||||
this.size = size;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for etag.
|
||||
*******************************************************************************/
|
||||
public String getEtag()
|
||||
{
|
||||
return (this.etag);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for etag.
|
||||
*******************************************************************************/
|
||||
public void setEtag(String etag)
|
||||
{
|
||||
this.etag = etag;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for etag.
|
||||
*******************************************************************************/
|
||||
public StoredObject withEtag(String etag)
|
||||
{
|
||||
this.etag = etag;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for lastModified.
|
||||
*******************************************************************************/
|
||||
public Instant getLastModified()
|
||||
{
|
||||
return (this.lastModified);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for lastModified.
|
||||
*******************************************************************************/
|
||||
public void setLastModified(Instant lastModified)
|
||||
{
|
||||
this.lastModified = lastModified;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for lastModified.
|
||||
*******************************************************************************/
|
||||
public StoredObject withLastModified(Instant lastModified)
|
||||
{
|
||||
this.lastModified = lastModified;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for headers.
|
||||
*******************************************************************************/
|
||||
public ObjectHeaders getHeaders()
|
||||
{
|
||||
return (this.headers);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for headers.
|
||||
*******************************************************************************/
|
||||
public void setHeaders(ObjectHeaders headers)
|
||||
{
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for headers.
|
||||
*******************************************************************************/
|
||||
public StoredObject withHeaders(ObjectHeaders headers)
|
||||
{
|
||||
this.headers = headers;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for versionId.
|
||||
*******************************************************************************/
|
||||
public String getVersionId()
|
||||
{
|
||||
return (this.versionId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for versionId.
|
||||
*******************************************************************************/
|
||||
public void setVersionId(String versionId)
|
||||
{
|
||||
this.versionId = versionId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for versionId.
|
||||
*******************************************************************************/
|
||||
public StoredObject withVersionId(String versionId)
|
||||
{
|
||||
this.versionId = versionId;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for storageClass.
|
||||
*******************************************************************************/
|
||||
public String getStorageClass()
|
||||
{
|
||||
return (this.storageClass);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for storageClass.
|
||||
*******************************************************************************/
|
||||
public void setStorageClass(String storageClass)
|
||||
{
|
||||
this.storageClass = storageClass;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for storageClass.
|
||||
*******************************************************************************/
|
||||
public StoredObject withStorageClass(String storageClass)
|
||||
{
|
||||
this.storageClass = storageClass;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for lockMode.
|
||||
*******************************************************************************/
|
||||
public String getLockMode()
|
||||
{
|
||||
return (this.lockMode);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for lockMode.
|
||||
*******************************************************************************/
|
||||
public void setLockMode(String lockMode)
|
||||
{
|
||||
this.lockMode = lockMode;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for lockMode.
|
||||
*******************************************************************************/
|
||||
public StoredObject withLockMode(String lockMode)
|
||||
{
|
||||
this.lockMode = lockMode;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for lockRetainUntil.
|
||||
*******************************************************************************/
|
||||
public Instant getLockRetainUntil()
|
||||
{
|
||||
return (this.lockRetainUntil);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for lockRetainUntil.
|
||||
*******************************************************************************/
|
||||
public void setLockRetainUntil(Instant lockRetainUntil)
|
||||
{
|
||||
this.lockRetainUntil = lockRetainUntil;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for lockRetainUntil.
|
||||
*******************************************************************************/
|
||||
public StoredObject withLockRetainUntil(Instant lockRetainUntil)
|
||||
{
|
||||
this.lockRetainUntil = lockRetainUntil;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.api;
|
||||
|
||||
|
||||
import com.kingsrook.qbits.objectstorage.internal.ConfigSync;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The storage API for a system caller (see ObjectStorage.asSystem): calls are
|
||||
** tagged with the calling qbit and run, skip the user's object_storage.*
|
||||
** permissions, and may use the system-only permanent delete.
|
||||
*******************************************************************************/
|
||||
public final class SystemStorage
|
||||
{
|
||||
private final StorageCaller caller;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor.
|
||||
*******************************************************************************/
|
||||
SystemStorage(StorageCaller caller)
|
||||
{
|
||||
this.caller = caller;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The bucket behind an alias, called as this system caller.
|
||||
*******************************************************************************/
|
||||
public StorageBucket bucket(String alias)
|
||||
{
|
||||
ConfigSync.ensureSynced();
|
||||
return (new StorageBucket(alias, caller));
|
||||
}
|
||||
}
|
||||
@@ -1,488 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.app;
|
||||
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.objectstorage.model.ObjectStorageTables;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The Storage home: full-width sections, stacked, readable at every width with
|
||||
** no inner horizontal scroll. It is the single place for storage data: every
|
||||
** number comes from the bucket rows, the object index or the operation log,
|
||||
** and every empty state says what is missing and why. Other apps link here;
|
||||
** nothing is duplicated.
|
||||
*******************************************************************************/
|
||||
public final class StorageHome
|
||||
{
|
||||
private static final DateTimeFormatter WHEN = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm 'UTC'").withZone(ZoneOffset.UTC);
|
||||
|
||||
private static final String WRAP = "display:flex;flex-direction:column;gap:1.25rem;width:100%;";
|
||||
private static final String CARD = "border:1px solid rgba(127,127,127,.3);border-radius:8px;padding:1rem;min-width:0;overflow-wrap:anywhere;";
|
||||
private static final String GRID = "display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,19rem),1fr));gap:.75rem;";
|
||||
private static final String ITEM = "border:1px solid rgba(127,127,127,.25);border-radius:6px;padding:.7rem .85rem;min-width:0;overflow-wrap:anywhere;";
|
||||
private static final String PAIRS = "display:grid;grid-template-columns:minmax(6.5rem,auto) minmax(0,1fr);gap:.2rem .75rem;margin:.5rem 0 0;font-size:.88rem;";
|
||||
private static final String CHIP = "display:inline-block;border:1px solid rgba(127,127,127,.45);border-radius:999px;padding:0 .5rem;margin:0 .25rem .25rem 0;font-size:.78rem;";
|
||||
private static final String MUTED = "opacity:.75;";
|
||||
private static final String ROW = "padding:.5rem 0;border-bottom:1px solid rgba(127,127,127,.2);min-width:0;overflow-wrap:anywhere;";
|
||||
private static final String HEAD = "display:flex;justify-content:space-between;gap:.5rem;align-items:baseline;";
|
||||
|
||||
/** Important capabilities whose absence or partial support is worth a chip. */
|
||||
private static final Map<String, String> CHIP_NAMES = Map.of("VERSIONING", "versioning", "LIFECYCLE", "lifecycle", "OBJECT_LOCK", "object lock",
|
||||
"OBJECT_TAGGING", "tagging", "PRESIGNED_POST", "presigned POST", "CONDITIONAL_WRITE", "conditional writes");
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private StorageHome()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Render the whole home, reading the current user's view of the tables (so
|
||||
** per-bucket scoping applies). tablePathPrefix is the prefix of table and
|
||||
** record links.
|
||||
*******************************************************************************/
|
||||
public static String render(String tablePathPrefix, Instant now) throws QException
|
||||
{
|
||||
List<QRecord> connections = query(ObjectStorageTables.CONNECTION, new QQueryFilter().withOrderBy(new com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy("name")));
|
||||
List<QRecord> buckets = query(ObjectStorageTables.BUCKET, new QQueryFilter().withOrderBy(new com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy("alias")));
|
||||
List<QRecord> caps = query(ObjectStorageTables.BUCKET_CAPABILITY, new QQueryFilter());
|
||||
List<QRecord> operations = query(ObjectStorageTables.OPERATION, new QQueryFilter().withOrderBy(new com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy("occurredAt", false)).withLimit(20));
|
||||
List<QRecord> usage = query(ObjectStorageTables.USAGE_DAILY, new QQueryFilter().withCriteria(new QFilterCriteria("snapshotDate", QCriteriaOperator.GREATER_THAN_OR_EQUALS,
|
||||
LocalDate.ofInstant(now, ZoneOffset.UTC).minusDays(30))).withOrderBy(new com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy("snapshotDate")));
|
||||
|
||||
StringBuilder html = new StringBuilder("<div class=\"object-storage-home\" style=\"" + WRAP + "\">");
|
||||
setupSection(html, tablePathPrefix);
|
||||
bucketsSection(html, buckets, connections, caps, tablePathPrefix);
|
||||
healthSection(html, buckets, tablePathPrefix);
|
||||
recentSection(html, operations, tablePathPrefix);
|
||||
headersSection(html, buckets, tablePathPrefix);
|
||||
usageSection(html, buckets, usage);
|
||||
connectionsSection(html, connections, tablePathPrefix, now);
|
||||
return (html.append("</div>").toString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Query a table (empty list when the user cannot see it).
|
||||
*******************************************************************************/
|
||||
private static List<QRecord> query(String table, QQueryFilter filter) throws QException
|
||||
{
|
||||
return (new QueryAction().execute(new QueryInput(table).withFilter(filter)).getRecords());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** 0. Setup: each configuration table, its state and the next step.
|
||||
*******************************************************************************/
|
||||
private static void setupSection(StringBuilder html, String prefix) throws QException
|
||||
{
|
||||
open(html, "Setup");
|
||||
for(StorageSetupStatus.Step step : StorageSetupStatus.load())
|
||||
{
|
||||
html.append("<div style=\"").append(ROW).append("\"><strong><a href=\"").append(esc(prefix + "/" + step.table())).append("\">").append(esc(step.label())).append("</a></strong>: ")
|
||||
.append(esc(step.state())).append(". <span style=\"").append(MUTED).append("\">").append(esc(step.next())).append("</span></div>");
|
||||
}
|
||||
close(html);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** 1. Buckets: one card per bucket, linking to its record. Cards flow into as
|
||||
** many columns as fit, so the section reads at any width without scrolling.
|
||||
*******************************************************************************/
|
||||
private static void bucketsSection(StringBuilder html, List<QRecord> buckets, List<QRecord> connections, List<QRecord> caps, String prefix)
|
||||
{
|
||||
open(html, "Buckets");
|
||||
if(buckets.isEmpty())
|
||||
{
|
||||
empty(html, "No buckets are configured. Declare them in the qbit config (withBucket) and restart.");
|
||||
close(html);
|
||||
return;
|
||||
}
|
||||
Map<Object, String> connectionNames = new LinkedHashMap<>();
|
||||
connections.forEach(c -> connectionNames.put(c.getValue("id"), c.getValueString("name")));
|
||||
html.append("<div style=\"").append(GRID).append("\">");
|
||||
for(QRecord bucket : buckets)
|
||||
{
|
||||
boolean indexed = Boolean.TRUE.equals(bucket.getValueBoolean("indexed"));
|
||||
html.append("<div style=\"").append(ITEM).append("\"><div style=\"").append(HEAD).append("\"><strong>")
|
||||
.append(link(prefix, ObjectStorageTables.BUCKET, bucket.getValue("id"), bucket.getValueString("alias"))).append("</strong><span style=\"").append(MUTED).append("font-size:.8rem;\">")
|
||||
.append(esc(bucket.getValueString("role"))).append("</span></div><dl style=\"").append(PAIRS).append("\">");
|
||||
pair(html, "Connection", esc(connectionNames.getOrDefault(bucket.getValue("connectionId"), "")));
|
||||
pair(html, "Objects", indexed ? number(bucket.getValueLong("objectCount")) : "<span style=\"" + MUTED + "\">not indexed</span>");
|
||||
pair(html, "Size", indexed ? bytes(bucket.getValueLong("totalBytes")) : "<span style=\"" + MUTED + "\">not indexed</span>");
|
||||
pair(html, "Last scan", bucket.getValueInstant("lastScanDate") == null ? "<span style=\"" + MUTED + "\">never</span>" : WHEN.format(bucket.getValueInstant("lastScanDate")));
|
||||
pair(html, "Versioning", esc(bucket.getValueString("versioningState") == null ? "not checked yet" : bucket.getValueString("versioningState")));
|
||||
html.append("</dl>");
|
||||
StringBuilder chips = new StringBuilder();
|
||||
for(QRecord cap : caps)
|
||||
{
|
||||
String capability = cap.getValueString("capability");
|
||||
boolean relevant = CHIP_NAMES.containsKey(capability) && (!"OBJECT_LOCK".equals(capability) || "BACKUP".equals(bucket.getValueString("role")));
|
||||
if(relevant && cap.getValueInteger("bucketId") != null && cap.getValueInteger("bucketId").equals(bucket.getValueInteger("id")))
|
||||
{
|
||||
String status = cap.getValueString("status");
|
||||
String name = CHIP_NAMES.get(capability);
|
||||
if("UNSUPPORTED".equals(status))
|
||||
{
|
||||
chips.append("<span style=\"").append(CHIP).append("\">no ").append(name).append("</span>");
|
||||
}
|
||||
else if("PARTIAL".equals(status))
|
||||
{
|
||||
chips.append("<span style=\"").append(CHIP).append("\">").append(name).append(" partial</span>");
|
||||
}
|
||||
else if("NOT_PERMITTED".equals(status))
|
||||
{
|
||||
chips.append("<span style=\"").append(CHIP).append("\">").append(name).append(": key not permitted</span>");
|
||||
}
|
||||
}
|
||||
}
|
||||
if(bucket.getValueString("configDriftNote") != null)
|
||||
{
|
||||
chips.append("<span style=\"").append(CHIP).append("\">config drift</span>");
|
||||
}
|
||||
if(chips.length() > 0)
|
||||
{
|
||||
html.append("<div style=\"margin-top:.55rem;\">").append(chips).append("</div>");
|
||||
}
|
||||
html.append("</div>");
|
||||
}
|
||||
html.append("</div>");
|
||||
close(html);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** 2. Data health: orphaned and missing objects per bucket.
|
||||
*******************************************************************************/
|
||||
private static void healthSection(StringBuilder html, List<QRecord> buckets, String prefix)
|
||||
{
|
||||
open(html, "Data health");
|
||||
List<QRecord> tracked = buckets.stream().filter(b -> Boolean.TRUE.equals(b.getValueBoolean("referenceTracked"))).toList();
|
||||
if(tracked.isEmpty())
|
||||
{
|
||||
empty(html, "No bucket tracks references yet, so orphaned and missing objects cannot be counted. Register a reference provider (for example FieldReferenceProvider) and mark the bucket referenceTracked.");
|
||||
close(html);
|
||||
return;
|
||||
}
|
||||
html.append("<div style=\"").append(GRID).append("\">");
|
||||
for(QRecord bucket : tracked)
|
||||
{
|
||||
boolean scanned = bucket.getValueInstant("lastReferenceScanDate") != null;
|
||||
String orphanFilter = filterLink(prefix, ObjectStorageTables.OBJECT, "[{\"fieldName\":\"bucketId\",\"operator\":\"EQUALS\",\"values\":[" + bucket.getValueInteger("id") + "]},"
|
||||
+ "{\"fieldName\":\"referenceCount\",\"operator\":\"EQUALS\",\"values\":[0]}]");
|
||||
String missingFilter = filterLink(prefix, ObjectStorageTables.OBJECT_REFERENCE, "[{\"fieldName\":\"bucketId\",\"operator\":\"EQUALS\",\"values\":[" + bucket.getValueInteger("id") + "]},"
|
||||
+ "{\"fieldName\":\"missing\",\"operator\":\"EQUALS\",\"values\":[true]}]");
|
||||
html.append("<div style=\"").append(ITEM).append("\"><strong>").append(link(prefix, ObjectStorageTables.BUCKET, bucket.getValue("id"), bucket.getValueString("alias"))).append("</strong><dl style=\"")
|
||||
.append(PAIRS).append("\">");
|
||||
pair(html, "Orphaned", scanned ? "<a href=\"" + esc(orphanFilter) + "\">" + number(bucket.getValueLong("orphanedCount")) + " objects</a> (" + bytes(bucket.getValueLong("orphanedBytes")) + ")"
|
||||
: "<span style=\"" + MUTED + "\">not scanned yet</span>");
|
||||
pair(html, "Missing", scanned ? "<a href=\"" + esc(missingFilter) + "\">" + number(bucket.getValueLong("missingCount")) + " objects</a>" : "");
|
||||
pair(html, "Last scan", scanned ? WHEN.format(bucket.getValueInstant("lastReferenceScanDate")) : "never");
|
||||
html.append("</dl></div>");
|
||||
}
|
||||
html.append("</div>");
|
||||
close(html);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** 3. Recent changes: the latest 20 operations, linking to the full table.
|
||||
*******************************************************************************/
|
||||
private static void recentSection(StringBuilder html, List<QRecord> operations, String prefix)
|
||||
{
|
||||
open(html, "Recent changes");
|
||||
if(operations.isEmpty())
|
||||
{
|
||||
empty(html, "No storage writes have been recorded yet. Every write from the app, an action or a scheduled job lands here.");
|
||||
close(html);
|
||||
return;
|
||||
}
|
||||
for(QRecord op : operations)
|
||||
{
|
||||
html.append("<div style=\"").append(ROW).append("\"><div><strong>").append(link(prefix, ObjectStorageTables.OPERATION, op.getValue("id"), String.valueOf(op.getValueString("action"))))
|
||||
.append("</strong> ").append(esc(op.getValueString("bucketAlias") + "/" + (op.getValueString("objectKey") == null ? "" : op.getValueString("objectKey")))).append("</div><div style=\"")
|
||||
.append(MUTED).append("font-size:.85rem;\">").append(op.getValueInstant("occurredAt") == null ? "" : WHEN.format(op.getValueInstant("occurredAt"))).append(" · ")
|
||||
.append(esc(op.getValueString("actor"))).append(" · ").append(esc(op.getValueString("result")))
|
||||
.append(op.getValueString("errorMessage") == null ? "" : ": " + esc(op.getValueString("errorMessage"))).append("</div></div>");
|
||||
}
|
||||
html.append("<p style=\"margin:.75rem 0 0;\"><a href=\"").append(esc(prefix + "/" + ObjectStorageTables.OPERATION)).append("\">See every operation</a></p>");
|
||||
close(html);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** 4. Cache headers: how many indexed objects match the header policy.
|
||||
*******************************************************************************/
|
||||
private static void headersSection(StringBuilder html, List<QRecord> buckets, String prefix)
|
||||
{
|
||||
open(html, "Cache headers");
|
||||
List<QRecord> indexed = buckets.stream().filter(b -> Boolean.TRUE.equals(b.getValueBoolean("indexed")) && !"BACKUP".equals(b.getValueString("role"))).toList();
|
||||
if(indexed.isEmpty())
|
||||
{
|
||||
empty(html, "No indexed bucket, so header coverage cannot be measured.");
|
||||
close(html);
|
||||
return;
|
||||
}
|
||||
for(QRecord bucket : indexed)
|
||||
{
|
||||
long total = bucket.getValueLong("objectCount") == null ? 0 : bucket.getValueLong("objectCount");
|
||||
long known = bucket.getValueLong("headersKnownCount") == null ? 0 : bucket.getValueLong("headersKnownCount");
|
||||
long match = bucket.getValueLong("headersMatchingPolicyCount") == null ? 0 : bucket.getValueLong("headersMatchingPolicyCount");
|
||||
html.append("<div style=\"").append(ROW).append("\"><strong>").append(link(prefix, ObjectStorageTables.BUCKET, bucket.getValue("id"), bucket.getValueString("alias"))).append("</strong>: ");
|
||||
if(bucket.getValueInstant("lastScanDate") == null)
|
||||
{
|
||||
html.append("<span style=\"").append(MUTED).append("\">not scanned yet</span>");
|
||||
}
|
||||
else
|
||||
{
|
||||
html.append(number(match)).append(" of ").append(number(total)).append(" objects match the header policy; headers known for ")
|
||||
.append(total == 0 ? 100 : Math.round(known * 100.0 / total)).append("%");
|
||||
}
|
||||
html.append("</div>");
|
||||
}
|
||||
html.append("<p style=\"margin:.75rem 0 0;\">To fix mismatches, run <strong>Apply header policy</strong> on the objects or a bucket. Edge caching lives in the CDN app.</p>");
|
||||
close(html);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** 5. Usage trend: bytes and object count per bucket over 30 days (snapshots).
|
||||
*******************************************************************************/
|
||||
private static void usageSection(StringBuilder html, List<QRecord> buckets, List<QRecord> usage)
|
||||
{
|
||||
open(html, "Usage trend");
|
||||
Map<Object, List<QRecord>> byBucket = new LinkedHashMap<>();
|
||||
for(QRecord row : usage)
|
||||
{
|
||||
byBucket.computeIfAbsent(row.getValue("bucketId"), k -> new ArrayList<>()).add(row);
|
||||
}
|
||||
StringBuilder trends = new StringBuilder();
|
||||
StringBuilder collecting = new StringBuilder();
|
||||
for(QRecord bucket : buckets)
|
||||
{
|
||||
List<QRecord> rows = byBucket.getOrDefault(bucket.getValue("id"), List.of());
|
||||
if(rows.size() < 2)
|
||||
{
|
||||
if(!rows.isEmpty())
|
||||
{
|
||||
collecting.append("<div style=\"").append(ROW).append("\"><strong>").append(esc(bucket.getValueString("alias"))).append("</strong>: collecting since ")
|
||||
.append(rows.get(0).getValueString("snapshotDate")).append("</div>");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
QRecord first = rows.get(0);
|
||||
QRecord last = rows.get(rows.size() - 1);
|
||||
long delta = (last.getValueLong("totalBytes") == null ? 0 : last.getValueLong("totalBytes")) - (first.getValueLong("totalBytes") == null ? 0 : first.getValueLong("totalBytes"));
|
||||
long objs = (last.getValueLong("objectCount") == null ? 0 : last.getValueLong("objectCount")) - (first.getValueLong("objectCount") == null ? 0 : first.getValueLong("objectCount"));
|
||||
trends.append("<div style=\"").append(ITEM).append("\"><strong>").append(esc(bucket.getValueString("alias"))).append("</strong><dl style=\"").append(PAIRS).append("\">");
|
||||
pair(trends, "Now", number(last.getValueLong("objectCount")) + " objects, " + bytes(last.getValueLong("totalBytes")));
|
||||
pair(trends, "Since " + first.getValueString("snapshotDate"), number(first.getValueLong("objectCount")) + " objects, " + bytes(first.getValueLong("totalBytes")));
|
||||
pair(trends, "Change", (objs >= 0 ? "+" : "") + objs + " objects, " + (delta >= 0 ? "+" : "-") + bytes(Math.abs(delta)));
|
||||
trends.append("</dl></div>");
|
||||
}
|
||||
if(trends.length() > 0)
|
||||
{
|
||||
html.append("<div style=\"").append(GRID).append("\">").append(trends).append("</div>");
|
||||
}
|
||||
html.append(collecting);
|
||||
if(trends.length() == 0 && collecting.length() == 0)
|
||||
{
|
||||
empty(html, "No usage snapshots yet. The nightly snapshot records bucket size and object count from the index; a trend appears after two days.");
|
||||
}
|
||||
close(html);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** 6. Connections: last test and result, with a nudge when never tested or old.
|
||||
*******************************************************************************/
|
||||
private static void connectionsSection(StringBuilder html, List<QRecord> connections, String prefix, Instant now)
|
||||
{
|
||||
open(html, "Connections");
|
||||
if(connections.isEmpty())
|
||||
{
|
||||
empty(html, "No connections are configured.");
|
||||
close(html);
|
||||
return;
|
||||
}
|
||||
html.append("<div style=\"").append(GRID).append("\">");
|
||||
for(QRecord c : connections)
|
||||
{
|
||||
Instant tested = c.getValueInstant("lastTestedAt");
|
||||
String nudge = tested == null ? "Never tested: run Test connection on the record."
|
||||
: ChronoUnit.DAYS.between(tested, now) > 30 ? "Last tested more than 30 days ago." : "";
|
||||
if(c.getValueString("presignWarning") != null)
|
||||
{
|
||||
nudge = (nudge.isEmpty() ? "" : nudge + " ") + c.getValueString("presignWarning");
|
||||
}
|
||||
html.append("<div style=\"").append(ITEM).append("\"><div style=\"").append(HEAD).append("\"><strong>")
|
||||
.append(link(prefix, ObjectStorageTables.CONNECTION, c.getValue("id"), c.getValueString("name"))).append("</strong><span style=\"").append(MUTED).append("font-size:.8rem;\">")
|
||||
.append(esc(c.getValueString("providerType"))).append("</span></div><dl style=\"").append(PAIRS).append("\">");
|
||||
pair(html, "Last test", tested == null ? "never" : WHEN.format(tested));
|
||||
pair(html, "Result", esc(c.getValueString("lastTestStatus") == null ? "" : c.getValueString("lastTestStatus")));
|
||||
if(!nudge.isEmpty())
|
||||
{
|
||||
pair(html, "Note", esc(nudge));
|
||||
}
|
||||
html.append("</dl></div>");
|
||||
}
|
||||
html.append("</div>");
|
||||
close(html);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** One label/value pair in a card.
|
||||
*******************************************************************************/
|
||||
private static void pair(StringBuilder html, String label, String valueHtml)
|
||||
{
|
||||
html.append("<dt style=\"").append(MUTED).append("\">").append(esc(label)).append("</dt><dd style=\"margin:0;min-width:0;\">").append(valueHtml).append("</dd>");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Open a card.
|
||||
*******************************************************************************/
|
||||
private static void open(StringBuilder html, String title)
|
||||
{
|
||||
html.append("<section style=\"").append(CARD).append("\"><h3 style=\"margin:0 0 .75rem;font-size:1.05rem;\">").append(esc(title)).append("</h3>");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Close a card.
|
||||
*******************************************************************************/
|
||||
private static void close(StringBuilder html)
|
||||
{
|
||||
html.append("</section>");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** An honest empty state.
|
||||
*******************************************************************************/
|
||||
private static void empty(StringBuilder html, String text)
|
||||
{
|
||||
html.append("<p style=\"margin:0;").append(MUTED).append("\">").append(esc(text)).append("</p>");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A link to a record.
|
||||
*******************************************************************************/
|
||||
private static String link(String prefix, String table, Object id, String text)
|
||||
{
|
||||
return ("<a href=\"" + esc(prefix + "/" + table + "/" + id) + "\">" + esc(text) + "</a>");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A link to a table filtered by criteria JSON.
|
||||
*******************************************************************************/
|
||||
private static String filterLink(String prefix, String table, String criteriaJson)
|
||||
{
|
||||
String filter = "{\"criteria\":" + criteriaJson + ",\"booleanOperator\":\"AND\"}";
|
||||
return (prefix + "/" + table + "?filter=" + URLEncoder.encode(filter, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A count with thousands separators.
|
||||
*******************************************************************************/
|
||||
private static String number(Long value)
|
||||
{
|
||||
return (value == null ? "0" : String.format("%,d", value));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Bytes as KB/MB/GB.
|
||||
*******************************************************************************/
|
||||
static String bytes(Long value)
|
||||
{
|
||||
if(value == null)
|
||||
{
|
||||
return ("0 B");
|
||||
}
|
||||
String[] units = { "B", "KB", "MB", "GB", "TB" };
|
||||
BigDecimal v = new BigDecimal(value);
|
||||
int unit = 0;
|
||||
while(v.compareTo(BigDecimal.valueOf(1024)) >= 0 && unit < units.length - 1)
|
||||
{
|
||||
v = v.divide(BigDecimal.valueOf(1024), 4, java.math.RoundingMode.HALF_UP);
|
||||
unit++;
|
||||
}
|
||||
return (unit == 0 ? value + " B" : v.setScale(1, java.math.RoundingMode.HALF_UP) + " " + units[unit]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** HTML-escape.
|
||||
*******************************************************************************/
|
||||
private static String esc(String s)
|
||||
{
|
||||
return (s == null ? "" : ValueUtils.getValueAsString(s).replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """));
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.app;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.objectstorage.ObjectStorageQBitConfig;
|
||||
import com.kingsrook.qbits.objectstorage.internal.Db;
|
||||
import com.kingsrook.qbits.objectstorage.internal.ObjectStorageRuntime;
|
||||
import com.kingsrook.qbits.objectstorage.model.ObjectStorageTables;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The Setup card on the Storage home: each configuration table, its state and
|
||||
** the next step. States use the same resolution the runtime uses: connections
|
||||
** and buckets come from code (so "set up" means declared and synced), and the
|
||||
** header policy is the host's own when the config names one, otherwise the
|
||||
** enabled rows of object_storage_header_rule.
|
||||
*******************************************************************************/
|
||||
public final class StorageSetupStatus
|
||||
{
|
||||
public static final String SET_UP = "Set up";
|
||||
public static final String NEEDS_SETUP = "Needs setup";
|
||||
public static final String USING_DEFAULTS = "Using defaults";
|
||||
public static final String OPTIONAL = "Optional";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** One line of the card.
|
||||
*******************************************************************************/
|
||||
public record Step(String label, String state, String next, String table)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private StorageSetupStatus()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Read the current state. Connections and buckets come from the runtime
|
||||
** config (what the runtime uses, whoever is looking), and the rule counts
|
||||
** are read as the system, so a bucket-scoped user is never shown a false
|
||||
** "needs setup".
|
||||
*******************************************************************************/
|
||||
public static List<Step> load() throws QException
|
||||
{
|
||||
ObjectStorageQBitConfig config = ObjectStorageRuntime.getConfig();
|
||||
int headerRules = Db.asSystem(() -> Db.query(ObjectStorageTables.HEADER_RULE, "enabled", true).size());
|
||||
int protection = Db.asSystem(() -> Db.query(ObjectStorageTables.PROTECTION_RULE, "enabled", true).size());
|
||||
return (steps(config.getConnections().size(), config.getBuckets().size(), headerRules, config.getHeaderPolicy() != null, protection));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The steps, from the numbers (pure, for tests).
|
||||
*******************************************************************************/
|
||||
static List<Step> steps(int connections, int buckets, int enabledHeaderRules, boolean hostHeaderPolicy, int enabledProtectionRules)
|
||||
{
|
||||
List<Step> steps = new ArrayList<>();
|
||||
steps.add(connections > 0
|
||||
? new Step("Connections", SET_UP, connections + " declared in code and synced here; set limits and timeouts on the row", ObjectStorageTables.CONNECTION)
|
||||
: new Step("Connections", NEEDS_SETUP, "Declare a connection in the qbit config (withConnection) and restart: endpoint, region and the names of the key variables", ObjectStorageTables.CONNECTION));
|
||||
steps.add(buckets > 0
|
||||
? new Step("Buckets", SET_UP, buckets + " declared in code and synced here; set the presign limits on the row", ObjectStorageTables.BUCKET)
|
||||
: new Step("Buckets", NEEDS_SETUP, "Declare a bucket in the qbit config (withBucket) and restart", ObjectStorageTables.BUCKET));
|
||||
if(hostHeaderPolicy)
|
||||
{
|
||||
steps.add(new Step("Header rules", SET_UP, "The host's own header policy (code) decides; these rows are not read", ObjectStorageTables.HEADER_RULE));
|
||||
}
|
||||
else if(enabledHeaderRules > 0)
|
||||
{
|
||||
steps.add(new Step("Header rules", SET_UP, enabledHeaderRules + " enabled; the first match by priority sets Cache-Control", ObjectStorageTables.HEADER_RULE));
|
||||
}
|
||||
else
|
||||
{
|
||||
steps.add(new Step("Header rules", USING_DEFAULTS, "No rule is enabled, so objects keep the headers they were written with. Enable the starter rule or add one", ObjectStorageTables.HEADER_RULE));
|
||||
}
|
||||
steps.add(enabledProtectionRules > 0
|
||||
? new Step("Protection rules", SET_UP, enabledProtectionRules + " enabled", ObjectStorageTables.PROTECTION_RULE)
|
||||
: new Step("Protection rules", OPTIONAL, "Add a rule to make a bucket prefix read-only or delete-proof", ObjectStorageTables.PROTECTION_RULE));
|
||||
return (steps);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.app;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.objectstorage.ObjectStorageQBitConfig;
|
||||
import com.kingsrook.qbits.objectstorage.internal.ConfigSync;
|
||||
import com.kingsrook.qbits.objectstorage.internal.ObjectStorageRuntime;
|
||||
import com.kingsrook.qbits.objectstorage.model.ObjectStorageTables;
|
||||
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.RawHTML;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The Admin -> Storage app: a home widget (the single owner of storage data)
|
||||
** and the nav of the main tables. Sub-tables (capabilities, folders, versions,
|
||||
** references, usage) appear as relations and child lists on their parents, not
|
||||
** in the nav. Processes are never added to the nav.
|
||||
*******************************************************************************/
|
||||
public final class StorageWidgets
|
||||
{
|
||||
public static final String HOME_WIDGET = "objectStorageHome";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private StorageWidgets()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The widgets.
|
||||
*******************************************************************************/
|
||||
public static List<QWidgetMetaData> widgets()
|
||||
{
|
||||
return (List.of(new QWidgetMetaData().withName(HOME_WIDGET).withLabel("Overview").withType(WidgetType.HTML.getType())
|
||||
.withCodeReference(new QCodeReference(HomeRenderer.class)).withGridColumns(12).withIsCard(false).withPermissionRules(ObjectStorageTables.permissionRules())));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The app. Nav entries are the home and the main tables.
|
||||
*******************************************************************************/
|
||||
public static QAppMetaData app(ObjectStorageQBitConfig config, List<QTableMetaData> tables)
|
||||
{
|
||||
QAppMetaData app = new QAppMetaData()
|
||||
.withName(config.getAppName())
|
||||
.withLabel(config.getAppLabel())
|
||||
.withIcon(new QIcon("inventory_2"))
|
||||
.withPermissionRules(ObjectStorageTables.permissionRules())
|
||||
.withWidgets(List.of(HOME_WIDGET))
|
||||
.withSection(new QAppSection().withName("storage").withLabel("Storage").withIcon(new QIcon("inventory_2"))
|
||||
.withTables(List.of(ObjectStorageTables.BUCKET, ObjectStorageTables.OBJECT, ObjectStorageTables.FOLDER, ObjectStorageTables.CONNECTION)))
|
||||
.withSection(new QAppSection().withName("activity").withLabel("Activity").withIcon(new QIcon("history"))
|
||||
.withTables(List.of(ObjectStorageTables.OPERATION)))
|
||||
.withSection(new QAppSection().withName("rules").withLabel("Rules").withIcon(new QIcon("rule"))
|
||||
.withTables(List.of(ObjectStorageTables.HEADER_RULE, ObjectStorageTables.PROTECTION_RULE)));
|
||||
for(String navTable : ObjectStorageTables.NAV_TABLES)
|
||||
{
|
||||
for(QTableMetaData table : tables)
|
||||
{
|
||||
if(table.getName().equals(navTable))
|
||||
{
|
||||
app.withChild(table);
|
||||
}
|
||||
}
|
||||
}
|
||||
return (app);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The prefix of table and record links.
|
||||
*******************************************************************************/
|
||||
public static String tablePathPrefix()
|
||||
{
|
||||
if(!ObjectStorageRuntime.isInstalled())
|
||||
{
|
||||
return ("/objectStorageAdmin");
|
||||
}
|
||||
ObjectStorageQBitConfig config = ObjectStorageRuntime.getConfig();
|
||||
return (config.getParentAppName() == null ? "/" + config.getAppName() : "/" + config.getParentAppName() + "/" + config.getAppName());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Storage home renderer.
|
||||
*******************************************************************************/
|
||||
public static class HomeRenderer extends AbstractWidgetRenderer
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Render.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
|
||||
{
|
||||
ConfigSync.ensureSynced();
|
||||
return (new RenderWidgetOutput(new RawHTML(null, StorageHome.render(tablePathPrefix(), Instant.now()))));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.config;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** How bucket names are put in request URLs.
|
||||
*******************************************************************************/
|
||||
public enum AddressingStyle
|
||||
{
|
||||
PATH,
|
||||
VIRTUAL_HOST
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.config;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Non-S3 admin APIs for usage and quotas (off by default).
|
||||
*******************************************************************************/
|
||||
public enum AdminApiType
|
||||
{
|
||||
NONE,
|
||||
MINIO_ADMIN,
|
||||
GARAGE_ADMIN
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.config;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** What a bucket is for. BACKUP is always private and is excluded from header policy and orphan scans.
|
||||
*******************************************************************************/
|
||||
public enum BucketRole
|
||||
{
|
||||
PUBLIC,
|
||||
PRIVATE,
|
||||
INCOMING,
|
||||
BACKUP
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.config;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Maps to the SDK v2 request checksum calculation and response checksum validation.
|
||||
*******************************************************************************/
|
||||
public enum ChecksumMode
|
||||
{
|
||||
WHEN_REQUIRED,
|
||||
WHEN_SUPPORTED
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.config;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Where credentials come from.
|
||||
*******************************************************************************/
|
||||
public enum CredentialsMode
|
||||
{
|
||||
STATIC_REFS,
|
||||
DEFAULT_CHAIN,
|
||||
WEB_IDENTITY
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.config;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Who owns a bucket's configuration: infrastructure-as-code (read-only here) or this UI.
|
||||
*******************************************************************************/
|
||||
public enum ManagedBy
|
||||
{
|
||||
IAC,
|
||||
UI
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.config;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Whether request payloads are hashed and signed.
|
||||
*******************************************************************************/
|
||||
public enum PayloadSigning
|
||||
{
|
||||
SIGNED,
|
||||
UNSIGNED_OVER_TLS
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.config;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** S3-compatible provider kinds. Seeds default capabilities and settings; the probe overrides them.
|
||||
*******************************************************************************/
|
||||
public enum ProviderType
|
||||
{
|
||||
AWS_S3,
|
||||
MINIO,
|
||||
CLOUDFLARE_R2,
|
||||
BACKBLAZE_B2,
|
||||
WASABI,
|
||||
DO_SPACES,
|
||||
CEPH_RGW,
|
||||
GARAGE,
|
||||
OTHER
|
||||
}
|
||||
@@ -1,721 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.config;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Configuration of one bucket alias: where it lives and what it is for.
|
||||
*******************************************************************************/
|
||||
public class StorageBucketConfig
|
||||
{
|
||||
private String alias;
|
||||
private String connection;
|
||||
private String bucketName;
|
||||
private BucketRole role = BucketRole.PRIVATE;
|
||||
private String publicBaseUrl;
|
||||
private Boolean indexed = true;
|
||||
private ManagedBy managedBy = ManagedBy.IAC;
|
||||
private Boolean trashEnabled = false;
|
||||
private String trashBucket;
|
||||
private String trashPrefix = ".trash/";
|
||||
private Integer trashRetentionDays = 30;
|
||||
private Integer maxPresignGetSeconds = 3600;
|
||||
private Integer maxPresignPutSeconds = 900;
|
||||
private Boolean referenceTracked = false;
|
||||
private java.util.List<String> exemptPrefixes = new java.util.ArrayList<>();
|
||||
private Long maxUploadBytes = 104_857_600L;
|
||||
private Boolean protectedBucket = false;
|
||||
private Boolean expectVersioning;
|
||||
private Boolean expectLifecycle;
|
||||
private Boolean expectCors;
|
||||
private Boolean expectPolicy;
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Default constructor.
|
||||
*******************************************************************************/
|
||||
public StorageBucketConfig()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for alias.
|
||||
*******************************************************************************/
|
||||
public String getAlias()
|
||||
{
|
||||
return (this.alias);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for alias.
|
||||
*******************************************************************************/
|
||||
public void setAlias(String alias)
|
||||
{
|
||||
this.alias = alias;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for alias.
|
||||
*******************************************************************************/
|
||||
public StorageBucketConfig withAlias(String alias)
|
||||
{
|
||||
this.alias = alias;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for connection.
|
||||
*******************************************************************************/
|
||||
public String getConnection()
|
||||
{
|
||||
return (this.connection);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for connection.
|
||||
*******************************************************************************/
|
||||
public void setConnection(String connection)
|
||||
{
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for connection.
|
||||
*******************************************************************************/
|
||||
public StorageBucketConfig withConnection(String connection)
|
||||
{
|
||||
this.connection = connection;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for bucketName.
|
||||
*******************************************************************************/
|
||||
public String getBucketName()
|
||||
{
|
||||
return (this.bucketName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for bucketName.
|
||||
*******************************************************************************/
|
||||
public void setBucketName(String bucketName)
|
||||
{
|
||||
this.bucketName = bucketName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for bucketName.
|
||||
*******************************************************************************/
|
||||
public StorageBucketConfig withBucketName(String bucketName)
|
||||
{
|
||||
this.bucketName = bucketName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for role.
|
||||
*******************************************************************************/
|
||||
public BucketRole getRole()
|
||||
{
|
||||
return (this.role);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for role.
|
||||
*******************************************************************************/
|
||||
public void setRole(BucketRole role)
|
||||
{
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for role.
|
||||
*******************************************************************************/
|
||||
public StorageBucketConfig withRole(BucketRole role)
|
||||
{
|
||||
this.role = role;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for publicBaseUrl.
|
||||
*******************************************************************************/
|
||||
public String getPublicBaseUrl()
|
||||
{
|
||||
return (this.publicBaseUrl);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for publicBaseUrl.
|
||||
*******************************************************************************/
|
||||
public void setPublicBaseUrl(String publicBaseUrl)
|
||||
{
|
||||
this.publicBaseUrl = publicBaseUrl;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for publicBaseUrl.
|
||||
*******************************************************************************/
|
||||
public StorageBucketConfig withPublicBaseUrl(String publicBaseUrl)
|
||||
{
|
||||
this.publicBaseUrl = publicBaseUrl;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for indexed.
|
||||
*******************************************************************************/
|
||||
public Boolean getIndexed()
|
||||
{
|
||||
return (this.indexed);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for indexed.
|
||||
*******************************************************************************/
|
||||
public void setIndexed(Boolean indexed)
|
||||
{
|
||||
this.indexed = indexed;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for indexed.
|
||||
*******************************************************************************/
|
||||
public StorageBucketConfig withIndexed(Boolean indexed)
|
||||
{
|
||||
this.indexed = indexed;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for managedBy.
|
||||
*******************************************************************************/
|
||||
public ManagedBy getManagedBy()
|
||||
{
|
||||
return (this.managedBy);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for managedBy.
|
||||
*******************************************************************************/
|
||||
public void setManagedBy(ManagedBy managedBy)
|
||||
{
|
||||
this.managedBy = managedBy;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for managedBy.
|
||||
*******************************************************************************/
|
||||
public StorageBucketConfig withManagedBy(ManagedBy managedBy)
|
||||
{
|
||||
this.managedBy = managedBy;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for trashEnabled.
|
||||
*******************************************************************************/
|
||||
public Boolean getTrashEnabled()
|
||||
{
|
||||
return (this.trashEnabled);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for trashEnabled.
|
||||
*******************************************************************************/
|
||||
public void setTrashEnabled(Boolean trashEnabled)
|
||||
{
|
||||
this.trashEnabled = trashEnabled;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for trashEnabled.
|
||||
*******************************************************************************/
|
||||
public StorageBucketConfig withTrashEnabled(Boolean trashEnabled)
|
||||
{
|
||||
this.trashEnabled = trashEnabled;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for trashBucket.
|
||||
*******************************************************************************/
|
||||
public String getTrashBucket()
|
||||
{
|
||||
return (this.trashBucket);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for trashBucket.
|
||||
*******************************************************************************/
|
||||
public void setTrashBucket(String trashBucket)
|
||||
{
|
||||
this.trashBucket = trashBucket;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for trashBucket.
|
||||
*******************************************************************************/
|
||||
public StorageBucketConfig withTrashBucket(String trashBucket)
|
||||
{
|
||||
this.trashBucket = trashBucket;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for trashPrefix.
|
||||
*******************************************************************************/
|
||||
public String getTrashPrefix()
|
||||
{
|
||||
return (this.trashPrefix);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for trashPrefix.
|
||||
*******************************************************************************/
|
||||
public void setTrashPrefix(String trashPrefix)
|
||||
{
|
||||
this.trashPrefix = trashPrefix;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for trashPrefix.
|
||||
*******************************************************************************/
|
||||
public StorageBucketConfig withTrashPrefix(String trashPrefix)
|
||||
{
|
||||
this.trashPrefix = trashPrefix;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for trashRetentionDays.
|
||||
*******************************************************************************/
|
||||
public Integer getTrashRetentionDays()
|
||||
{
|
||||
return (this.trashRetentionDays);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for trashRetentionDays.
|
||||
*******************************************************************************/
|
||||
public void setTrashRetentionDays(Integer trashRetentionDays)
|
||||
{
|
||||
this.trashRetentionDays = trashRetentionDays;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for trashRetentionDays.
|
||||
*******************************************************************************/
|
||||
public StorageBucketConfig withTrashRetentionDays(Integer trashRetentionDays)
|
||||
{
|
||||
this.trashRetentionDays = trashRetentionDays;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for maxPresignGetSeconds.
|
||||
*******************************************************************************/
|
||||
public Integer getMaxPresignGetSeconds()
|
||||
{
|
||||
return (this.maxPresignGetSeconds);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for maxPresignGetSeconds.
|
||||
*******************************************************************************/
|
||||
public void setMaxPresignGetSeconds(Integer maxPresignGetSeconds)
|
||||
{
|
||||
this.maxPresignGetSeconds = maxPresignGetSeconds;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for maxPresignGetSeconds.
|
||||
*******************************************************************************/
|
||||
public StorageBucketConfig withMaxPresignGetSeconds(Integer maxPresignGetSeconds)
|
||||
{
|
||||
this.maxPresignGetSeconds = maxPresignGetSeconds;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for maxPresignPutSeconds.
|
||||
*******************************************************************************/
|
||||
public Integer getMaxPresignPutSeconds()
|
||||
{
|
||||
return (this.maxPresignPutSeconds);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for maxPresignPutSeconds.
|
||||
*******************************************************************************/
|
||||
public void setMaxPresignPutSeconds(Integer maxPresignPutSeconds)
|
||||
{
|
||||
this.maxPresignPutSeconds = maxPresignPutSeconds;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for maxPresignPutSeconds.
|
||||
*******************************************************************************/
|
||||
public StorageBucketConfig withMaxPresignPutSeconds(Integer maxPresignPutSeconds)
|
||||
{
|
||||
this.maxPresignPutSeconds = maxPresignPutSeconds;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for referenceTracked.
|
||||
*******************************************************************************/
|
||||
public Boolean getReferenceTracked()
|
||||
{
|
||||
return (this.referenceTracked);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for referenceTracked.
|
||||
*******************************************************************************/
|
||||
public void setReferenceTracked(Boolean referenceTracked)
|
||||
{
|
||||
this.referenceTracked = referenceTracked;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for referenceTracked.
|
||||
*******************************************************************************/
|
||||
public StorageBucketConfig withReferenceTracked(Boolean referenceTracked)
|
||||
{
|
||||
this.referenceTracked = referenceTracked;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for exemptPrefixes.
|
||||
*******************************************************************************/
|
||||
public java.util.List<String> getExemptPrefixes()
|
||||
{
|
||||
return (this.exemptPrefixes);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for exemptPrefixes.
|
||||
*******************************************************************************/
|
||||
public void setExemptPrefixes(java.util.List<String> exemptPrefixes)
|
||||
{
|
||||
this.exemptPrefixes = exemptPrefixes;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for exemptPrefixes.
|
||||
*******************************************************************************/
|
||||
public StorageBucketConfig withExemptPrefixes(java.util.List<String> exemptPrefixes)
|
||||
{
|
||||
this.exemptPrefixes = exemptPrefixes;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for maxUploadBytes.
|
||||
*******************************************************************************/
|
||||
public Long getMaxUploadBytes()
|
||||
{
|
||||
return (this.maxUploadBytes);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for maxUploadBytes.
|
||||
*******************************************************************************/
|
||||
public void setMaxUploadBytes(Long maxUploadBytes)
|
||||
{
|
||||
this.maxUploadBytes = maxUploadBytes;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for maxUploadBytes.
|
||||
*******************************************************************************/
|
||||
public StorageBucketConfig withMaxUploadBytes(Long maxUploadBytes)
|
||||
{
|
||||
this.maxUploadBytes = maxUploadBytes;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for protectedBucket.
|
||||
*******************************************************************************/
|
||||
public Boolean getProtectedBucket()
|
||||
{
|
||||
return (this.protectedBucket);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for protectedBucket.
|
||||
*******************************************************************************/
|
||||
public void setProtectedBucket(Boolean protectedBucket)
|
||||
{
|
||||
this.protectedBucket = protectedBucket;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for protectedBucket.
|
||||
*******************************************************************************/
|
||||
public StorageBucketConfig withProtectedBucket(Boolean protectedBucket)
|
||||
{
|
||||
this.protectedBucket = protectedBucket;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for expectVersioning.
|
||||
*******************************************************************************/
|
||||
public Boolean getExpectVersioning()
|
||||
{
|
||||
return (this.expectVersioning);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for expectVersioning.
|
||||
*******************************************************************************/
|
||||
public void setExpectVersioning(Boolean expectVersioning)
|
||||
{
|
||||
this.expectVersioning = expectVersioning;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for expectVersioning.
|
||||
*******************************************************************************/
|
||||
public StorageBucketConfig withExpectVersioning(Boolean expectVersioning)
|
||||
{
|
||||
this.expectVersioning = expectVersioning;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for expectLifecycle.
|
||||
*******************************************************************************/
|
||||
public Boolean getExpectLifecycle()
|
||||
{
|
||||
return (this.expectLifecycle);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for expectLifecycle.
|
||||
*******************************************************************************/
|
||||
public void setExpectLifecycle(Boolean expectLifecycle)
|
||||
{
|
||||
this.expectLifecycle = expectLifecycle;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for expectLifecycle.
|
||||
*******************************************************************************/
|
||||
public StorageBucketConfig withExpectLifecycle(Boolean expectLifecycle)
|
||||
{
|
||||
this.expectLifecycle = expectLifecycle;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for expectCors.
|
||||
*******************************************************************************/
|
||||
public Boolean getExpectCors()
|
||||
{
|
||||
return (this.expectCors);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for expectCors.
|
||||
*******************************************************************************/
|
||||
public void setExpectCors(Boolean expectCors)
|
||||
{
|
||||
this.expectCors = expectCors;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for expectCors.
|
||||
*******************************************************************************/
|
||||
public StorageBucketConfig withExpectCors(Boolean expectCors)
|
||||
{
|
||||
this.expectCors = expectCors;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for expectPolicy.
|
||||
*******************************************************************************/
|
||||
public Boolean getExpectPolicy()
|
||||
{
|
||||
return (this.expectPolicy);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for expectPolicy.
|
||||
*******************************************************************************/
|
||||
public void setExpectPolicy(Boolean expectPolicy)
|
||||
{
|
||||
this.expectPolicy = expectPolicy;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for expectPolicy.
|
||||
*******************************************************************************/
|
||||
public StorageBucketConfig withExpectPolicy(Boolean expectPolicy)
|
||||
{
|
||||
this.expectPolicy = expectPolicy;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor with the alias.
|
||||
*******************************************************************************/
|
||||
public StorageBucketConfig(String alias)
|
||||
{
|
||||
this.alias = alias;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,871 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.config;
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Configuration of one S3-compatible connection. Secrets are references
|
||||
** ** (for example ${env.S3_SECRET_KEY}), never values.
|
||||
*******************************************************************************/
|
||||
public class StorageConnectionConfig
|
||||
{
|
||||
private String name;
|
||||
private ProviderType providerType = ProviderType.OTHER;
|
||||
private String endpoint;
|
||||
private String presignEndpoint;
|
||||
private String region;
|
||||
private AddressingStyle addressingStyle;
|
||||
private String signatureVersion = "V4";
|
||||
private ChecksumMode checksumMode;
|
||||
private PayloadSigning payloadSigning = PayloadSigning.SIGNED;
|
||||
private Boolean tlsVerify = true;
|
||||
private String caBundleRef;
|
||||
private CredentialsMode credentialsMode = CredentialsMode.STATIC_REFS;
|
||||
private String accessKeyRef;
|
||||
private String secretKeyRef;
|
||||
private String sessionTokenRef;
|
||||
private String presignAccessKeyRef;
|
||||
private String presignSecretKeyRef;
|
||||
private AdminApiType adminApiType = AdminApiType.NONE;
|
||||
private Integer maxOpsPerSecond = 20;
|
||||
private Integer maxConcurrency = 4;
|
||||
private Integer connectTimeoutMs = 5000;
|
||||
private Integer readTimeoutMs = 60000;
|
||||
private Integer multipartPartSizeMb = 16;
|
||||
private Long multipartCopyThresholdBytes = 5_368_709_120L;
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Default constructor.
|
||||
*******************************************************************************/
|
||||
public StorageConnectionConfig()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for name.
|
||||
*******************************************************************************/
|
||||
public String getName()
|
||||
{
|
||||
return (this.name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for name.
|
||||
*******************************************************************************/
|
||||
public void setName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for name.
|
||||
*******************************************************************************/
|
||||
public StorageConnectionConfig withName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for providerType.
|
||||
*******************************************************************************/
|
||||
public ProviderType getProviderType()
|
||||
{
|
||||
return (this.providerType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for providerType.
|
||||
*******************************************************************************/
|
||||
public void setProviderType(ProviderType providerType)
|
||||
{
|
||||
this.providerType = providerType;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for providerType.
|
||||
*******************************************************************************/
|
||||
public StorageConnectionConfig withProviderType(ProviderType providerType)
|
||||
{
|
||||
this.providerType = providerType;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for endpoint.
|
||||
*******************************************************************************/
|
||||
public String getEndpoint()
|
||||
{
|
||||
return (this.endpoint);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for endpoint.
|
||||
*******************************************************************************/
|
||||
public void setEndpoint(String endpoint)
|
||||
{
|
||||
this.endpoint = endpoint;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for endpoint.
|
||||
*******************************************************************************/
|
||||
public StorageConnectionConfig withEndpoint(String endpoint)
|
||||
{
|
||||
this.endpoint = endpoint;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for presignEndpoint.
|
||||
*******************************************************************************/
|
||||
public String getPresignEndpoint()
|
||||
{
|
||||
return (this.presignEndpoint);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for presignEndpoint.
|
||||
*******************************************************************************/
|
||||
public void setPresignEndpoint(String presignEndpoint)
|
||||
{
|
||||
this.presignEndpoint = presignEndpoint;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for presignEndpoint.
|
||||
*******************************************************************************/
|
||||
public StorageConnectionConfig withPresignEndpoint(String presignEndpoint)
|
||||
{
|
||||
this.presignEndpoint = presignEndpoint;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for region.
|
||||
*******************************************************************************/
|
||||
public String getRegion()
|
||||
{
|
||||
return (this.region);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for region.
|
||||
*******************************************************************************/
|
||||
public void setRegion(String region)
|
||||
{
|
||||
this.region = region;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for region.
|
||||
*******************************************************************************/
|
||||
public StorageConnectionConfig withRegion(String region)
|
||||
{
|
||||
this.region = region;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for addressingStyle.
|
||||
*******************************************************************************/
|
||||
public AddressingStyle getAddressingStyle()
|
||||
{
|
||||
return (this.addressingStyle);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for addressingStyle.
|
||||
*******************************************************************************/
|
||||
public void setAddressingStyle(AddressingStyle addressingStyle)
|
||||
{
|
||||
this.addressingStyle = addressingStyle;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for addressingStyle.
|
||||
*******************************************************************************/
|
||||
public StorageConnectionConfig withAddressingStyle(AddressingStyle addressingStyle)
|
||||
{
|
||||
this.addressingStyle = addressingStyle;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for signatureVersion.
|
||||
*******************************************************************************/
|
||||
public String getSignatureVersion()
|
||||
{
|
||||
return (this.signatureVersion);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for signatureVersion.
|
||||
*******************************************************************************/
|
||||
public void setSignatureVersion(String signatureVersion)
|
||||
{
|
||||
this.signatureVersion = signatureVersion;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for signatureVersion.
|
||||
*******************************************************************************/
|
||||
public StorageConnectionConfig withSignatureVersion(String signatureVersion)
|
||||
{
|
||||
this.signatureVersion = signatureVersion;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for checksumMode.
|
||||
*******************************************************************************/
|
||||
public ChecksumMode getChecksumMode()
|
||||
{
|
||||
return (this.checksumMode);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for checksumMode.
|
||||
*******************************************************************************/
|
||||
public void setChecksumMode(ChecksumMode checksumMode)
|
||||
{
|
||||
this.checksumMode = checksumMode;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for checksumMode.
|
||||
*******************************************************************************/
|
||||
public StorageConnectionConfig withChecksumMode(ChecksumMode checksumMode)
|
||||
{
|
||||
this.checksumMode = checksumMode;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for payloadSigning.
|
||||
*******************************************************************************/
|
||||
public PayloadSigning getPayloadSigning()
|
||||
{
|
||||
return (this.payloadSigning);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for payloadSigning.
|
||||
*******************************************************************************/
|
||||
public void setPayloadSigning(PayloadSigning payloadSigning)
|
||||
{
|
||||
this.payloadSigning = payloadSigning;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for payloadSigning.
|
||||
*******************************************************************************/
|
||||
public StorageConnectionConfig withPayloadSigning(PayloadSigning payloadSigning)
|
||||
{
|
||||
this.payloadSigning = payloadSigning;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for tlsVerify.
|
||||
*******************************************************************************/
|
||||
public Boolean getTlsVerify()
|
||||
{
|
||||
return (this.tlsVerify);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for tlsVerify.
|
||||
*******************************************************************************/
|
||||
public void setTlsVerify(Boolean tlsVerify)
|
||||
{
|
||||
this.tlsVerify = tlsVerify;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for tlsVerify.
|
||||
*******************************************************************************/
|
||||
public StorageConnectionConfig withTlsVerify(Boolean tlsVerify)
|
||||
{
|
||||
this.tlsVerify = tlsVerify;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for caBundleRef.
|
||||
*******************************************************************************/
|
||||
public String getCaBundleRef()
|
||||
{
|
||||
return (this.caBundleRef);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for caBundleRef.
|
||||
*******************************************************************************/
|
||||
public void setCaBundleRef(String caBundleRef)
|
||||
{
|
||||
this.caBundleRef = caBundleRef;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for caBundleRef.
|
||||
*******************************************************************************/
|
||||
public StorageConnectionConfig withCaBundleRef(String caBundleRef)
|
||||
{
|
||||
this.caBundleRef = caBundleRef;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for credentialsMode.
|
||||
*******************************************************************************/
|
||||
public CredentialsMode getCredentialsMode()
|
||||
{
|
||||
return (this.credentialsMode);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for credentialsMode.
|
||||
*******************************************************************************/
|
||||
public void setCredentialsMode(CredentialsMode credentialsMode)
|
||||
{
|
||||
this.credentialsMode = credentialsMode;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for credentialsMode.
|
||||
*******************************************************************************/
|
||||
public StorageConnectionConfig withCredentialsMode(CredentialsMode credentialsMode)
|
||||
{
|
||||
this.credentialsMode = credentialsMode;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for accessKeyRef.
|
||||
*******************************************************************************/
|
||||
public String getAccessKeyRef()
|
||||
{
|
||||
return (this.accessKeyRef);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for accessKeyRef.
|
||||
*******************************************************************************/
|
||||
public void setAccessKeyRef(String accessKeyRef)
|
||||
{
|
||||
this.accessKeyRef = accessKeyRef;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for accessKeyRef.
|
||||
*******************************************************************************/
|
||||
public StorageConnectionConfig withAccessKeyRef(String accessKeyRef)
|
||||
{
|
||||
this.accessKeyRef = accessKeyRef;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for secretKeyRef.
|
||||
*******************************************************************************/
|
||||
public String getSecretKeyRef()
|
||||
{
|
||||
return (this.secretKeyRef);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for secretKeyRef.
|
||||
*******************************************************************************/
|
||||
public void setSecretKeyRef(String secretKeyRef)
|
||||
{
|
||||
this.secretKeyRef = secretKeyRef;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for secretKeyRef.
|
||||
*******************************************************************************/
|
||||
public StorageConnectionConfig withSecretKeyRef(String secretKeyRef)
|
||||
{
|
||||
this.secretKeyRef = secretKeyRef;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for sessionTokenRef.
|
||||
*******************************************************************************/
|
||||
public String getSessionTokenRef()
|
||||
{
|
||||
return (this.sessionTokenRef);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for sessionTokenRef.
|
||||
*******************************************************************************/
|
||||
public void setSessionTokenRef(String sessionTokenRef)
|
||||
{
|
||||
this.sessionTokenRef = sessionTokenRef;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for sessionTokenRef.
|
||||
*******************************************************************************/
|
||||
public StorageConnectionConfig withSessionTokenRef(String sessionTokenRef)
|
||||
{
|
||||
this.sessionTokenRef = sessionTokenRef;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for presignAccessKeyRef.
|
||||
*******************************************************************************/
|
||||
public String getPresignAccessKeyRef()
|
||||
{
|
||||
return (this.presignAccessKeyRef);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for presignAccessKeyRef.
|
||||
*******************************************************************************/
|
||||
public void setPresignAccessKeyRef(String presignAccessKeyRef)
|
||||
{
|
||||
this.presignAccessKeyRef = presignAccessKeyRef;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for presignAccessKeyRef.
|
||||
*******************************************************************************/
|
||||
public StorageConnectionConfig withPresignAccessKeyRef(String presignAccessKeyRef)
|
||||
{
|
||||
this.presignAccessKeyRef = presignAccessKeyRef;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for presignSecretKeyRef.
|
||||
*******************************************************************************/
|
||||
public String getPresignSecretKeyRef()
|
||||
{
|
||||
return (this.presignSecretKeyRef);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for presignSecretKeyRef.
|
||||
*******************************************************************************/
|
||||
public void setPresignSecretKeyRef(String presignSecretKeyRef)
|
||||
{
|
||||
this.presignSecretKeyRef = presignSecretKeyRef;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for presignSecretKeyRef.
|
||||
*******************************************************************************/
|
||||
public StorageConnectionConfig withPresignSecretKeyRef(String presignSecretKeyRef)
|
||||
{
|
||||
this.presignSecretKeyRef = presignSecretKeyRef;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for adminApiType.
|
||||
*******************************************************************************/
|
||||
public AdminApiType getAdminApiType()
|
||||
{
|
||||
return (this.adminApiType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for adminApiType.
|
||||
*******************************************************************************/
|
||||
public void setAdminApiType(AdminApiType adminApiType)
|
||||
{
|
||||
this.adminApiType = adminApiType;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for adminApiType.
|
||||
*******************************************************************************/
|
||||
public StorageConnectionConfig withAdminApiType(AdminApiType adminApiType)
|
||||
{
|
||||
this.adminApiType = adminApiType;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for maxOpsPerSecond.
|
||||
*******************************************************************************/
|
||||
public Integer getMaxOpsPerSecond()
|
||||
{
|
||||
return (this.maxOpsPerSecond);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for maxOpsPerSecond.
|
||||
*******************************************************************************/
|
||||
public void setMaxOpsPerSecond(Integer maxOpsPerSecond)
|
||||
{
|
||||
this.maxOpsPerSecond = maxOpsPerSecond;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for maxOpsPerSecond.
|
||||
*******************************************************************************/
|
||||
public StorageConnectionConfig withMaxOpsPerSecond(Integer maxOpsPerSecond)
|
||||
{
|
||||
this.maxOpsPerSecond = maxOpsPerSecond;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for maxConcurrency.
|
||||
*******************************************************************************/
|
||||
public Integer getMaxConcurrency()
|
||||
{
|
||||
return (this.maxConcurrency);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for maxConcurrency.
|
||||
*******************************************************************************/
|
||||
public void setMaxConcurrency(Integer maxConcurrency)
|
||||
{
|
||||
this.maxConcurrency = maxConcurrency;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for maxConcurrency.
|
||||
*******************************************************************************/
|
||||
public StorageConnectionConfig withMaxConcurrency(Integer maxConcurrency)
|
||||
{
|
||||
this.maxConcurrency = maxConcurrency;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for connectTimeoutMs.
|
||||
*******************************************************************************/
|
||||
public Integer getConnectTimeoutMs()
|
||||
{
|
||||
return (this.connectTimeoutMs);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for connectTimeoutMs.
|
||||
*******************************************************************************/
|
||||
public void setConnectTimeoutMs(Integer connectTimeoutMs)
|
||||
{
|
||||
this.connectTimeoutMs = connectTimeoutMs;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for connectTimeoutMs.
|
||||
*******************************************************************************/
|
||||
public StorageConnectionConfig withConnectTimeoutMs(Integer connectTimeoutMs)
|
||||
{
|
||||
this.connectTimeoutMs = connectTimeoutMs;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for readTimeoutMs.
|
||||
*******************************************************************************/
|
||||
public Integer getReadTimeoutMs()
|
||||
{
|
||||
return (this.readTimeoutMs);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for readTimeoutMs.
|
||||
*******************************************************************************/
|
||||
public void setReadTimeoutMs(Integer readTimeoutMs)
|
||||
{
|
||||
this.readTimeoutMs = readTimeoutMs;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for readTimeoutMs.
|
||||
*******************************************************************************/
|
||||
public StorageConnectionConfig withReadTimeoutMs(Integer readTimeoutMs)
|
||||
{
|
||||
this.readTimeoutMs = readTimeoutMs;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for multipartPartSizeMb.
|
||||
*******************************************************************************/
|
||||
public Integer getMultipartPartSizeMb()
|
||||
{
|
||||
return (this.multipartPartSizeMb);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for multipartPartSizeMb.
|
||||
*******************************************************************************/
|
||||
public void setMultipartPartSizeMb(Integer multipartPartSizeMb)
|
||||
{
|
||||
this.multipartPartSizeMb = multipartPartSizeMb;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for multipartPartSizeMb.
|
||||
*******************************************************************************/
|
||||
public StorageConnectionConfig withMultipartPartSizeMb(Integer multipartPartSizeMb)
|
||||
{
|
||||
this.multipartPartSizeMb = multipartPartSizeMb;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Getter for multipartCopyThresholdBytes.
|
||||
*******************************************************************************/
|
||||
public Long getMultipartCopyThresholdBytes()
|
||||
{
|
||||
return (this.multipartCopyThresholdBytes);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Setter for multipartCopyThresholdBytes.
|
||||
*******************************************************************************/
|
||||
public void setMultipartCopyThresholdBytes(Long multipartCopyThresholdBytes)
|
||||
{
|
||||
this.multipartCopyThresholdBytes = multipartCopyThresholdBytes;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
** Fluent setter for multipartCopyThresholdBytes.
|
||||
*******************************************************************************/
|
||||
public StorageConnectionConfig withMultipartCopyThresholdBytes(Long multipartCopyThresholdBytes)
|
||||
{
|
||||
this.multipartCopyThresholdBytes = multipartCopyThresholdBytes;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor with the connection name.
|
||||
*******************************************************************************/
|
||||
public StorageConnectionConfig(String name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The addressing style, defaulting by provider (path for self-hosted stores).
|
||||
*******************************************************************************/
|
||||
public AddressingStyle effectiveAddressingStyle()
|
||||
{
|
||||
if(addressingStyle != null)
|
||||
{
|
||||
return (addressingStyle);
|
||||
}
|
||||
return (switch(providerType)
|
||||
{
|
||||
case AWS_S3, CLOUDFLARE_R2, BACKBLAZE_B2, WASABI, DO_SPACES -> AddressingStyle.VIRTUAL_HOST;
|
||||
default -> AddressingStyle.PATH;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The checksum mode, defaulting to WHEN_REQUIRED for every provider except AWS.
|
||||
*******************************************************************************/
|
||||
public ChecksumMode effectiveChecksumMode()
|
||||
{
|
||||
if(checksumMode != null)
|
||||
{
|
||||
return (checksumMode);
|
||||
}
|
||||
return (providerType == ProviderType.AWS_S3 ? ChecksumMode.WHEN_SUPPORTED : ChecksumMode.WHEN_REQUIRED);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether a separate presign key is configured.
|
||||
*******************************************************************************/
|
||||
public boolean hasPresignKey()
|
||||
{
|
||||
return (presignAccessKeyRef != null && presignSecretKeyRef != null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Redacted: only the name and non-secret settings.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return ("StorageConnectionConfig{name=" + name + ", providerType=" + providerType + ", endpoint=" + endpoint + ", credentials=[redacted]}");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.index;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.objectstorage.internal.Db;
|
||||
import com.kingsrook.qbits.objectstorage.model.ObjectStorageTables;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The folder tree of an indexed bucket: one object_storage_folder row per
|
||||
** prefix, with parentFolderId giving the tree and object count, bytes and
|
||||
** newest change rolled up (a folder counts everything beneath it).
|
||||
*******************************************************************************/
|
||||
public final class FolderIndex
|
||||
{
|
||||
private final Integer bucketId;
|
||||
private final Map<String, Integer> idByPath = new HashMap<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor.
|
||||
*******************************************************************************/
|
||||
private FolderIndex(Integer bucketId)
|
||||
{
|
||||
this.bucketId = bucketId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** An index with nothing cached: each folder is looked up on demand (for
|
||||
** one-off write-through, where loading the whole tree would be wasteful).
|
||||
*******************************************************************************/
|
||||
public static FolderIndex empty(Integer bucketId)
|
||||
{
|
||||
return (new FolderIndex(bucketId));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Load every existing folder of the bucket (cheap next to the object rows).
|
||||
*******************************************************************************/
|
||||
public static FolderIndex load(Integer bucketId) throws QException
|
||||
{
|
||||
FolderIndex index = new FolderIndex(bucketId);
|
||||
Db.forEachPage(ObjectStorageTables.FOLDER, new QQueryFilter(Db.eq("bucketId", bucketId)), 5000, rows ->
|
||||
{
|
||||
for(QRecord row : rows)
|
||||
{
|
||||
index.idByPath.put(row.getValueString("path"), row.getValueInteger("id"));
|
||||
}
|
||||
});
|
||||
return (index);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The folder id of a key's parent prefix, creating the chain of folders as
|
||||
** needed. Null for a key at the root.
|
||||
*******************************************************************************/
|
||||
public Integer ensureFor(String key) throws QException
|
||||
{
|
||||
Integer parentId = null;
|
||||
int slash = key.indexOf('/');
|
||||
while(slash >= 0)
|
||||
{
|
||||
String path = key.substring(0, slash + 1);
|
||||
Integer id = idByPath.get(path);
|
||||
if(id == null)
|
||||
{
|
||||
QRecord existing = Db.first(ObjectStorageTables.FOLDER, "bucketId", bucketId, "path", path);
|
||||
if(existing != null)
|
||||
{
|
||||
id = existing.getValueInteger("id");
|
||||
}
|
||||
else
|
||||
{
|
||||
String trimmed = path.substring(0, path.length() - 1);
|
||||
id = Db.insert(ObjectStorageTables.FOLDER, new QRecord().withValue("bucketId", bucketId).withValue("parentFolderId", parentId).withValue("path", path)
|
||||
.withValue("name", trimmed.substring(trimmed.lastIndexOf('/') + 1)).withValue("objectCount", 0L).withValue("totalBytes", 0L)).getValueInteger("id");
|
||||
}
|
||||
idByPath.put(path, id);
|
||||
}
|
||||
parentId = id;
|
||||
slash = key.indexOf('/', slash + 1);
|
||||
}
|
||||
return (parentId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The id of a folder path, or null.
|
||||
*******************************************************************************/
|
||||
public Integer idOf(String path)
|
||||
{
|
||||
return (idByPath.get(path));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Folder counts and sizes, direct (per folder) and the grand total.
|
||||
*******************************************************************************/
|
||||
public static class Rollup
|
||||
{
|
||||
final Map<Integer, long[]> direct = new HashMap<>();
|
||||
final Map<Integer, Instant> newest = new HashMap<>();
|
||||
long totalCount = 0;
|
||||
long totalBytes = 0;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Add one live object.
|
||||
*******************************************************************************/
|
||||
public void add(Integer folderId, Long size, Instant lastModified)
|
||||
{
|
||||
totalCount++;
|
||||
totalBytes += size == null ? 0 : size;
|
||||
if(folderId != null)
|
||||
{
|
||||
long[] c = direct.computeIfAbsent(folderId, k -> new long[2]);
|
||||
c[0]++;
|
||||
c[1] += size == null ? 0 : size;
|
||||
if(lastModified != null && (newest.get(folderId) == null || lastModified.isAfter(newest.get(folderId))))
|
||||
{
|
||||
newest.put(folderId, lastModified);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Write every folder's rolled-up totals: a folder counts everything beneath it.
|
||||
*******************************************************************************/
|
||||
public static void writeRollups(Integer bucketId, Rollup rollup) throws QException
|
||||
{
|
||||
Map<Integer, Integer> parentOf = new HashMap<>();
|
||||
List<Integer> ids = new ArrayList<>();
|
||||
Db.forEachPage(ObjectStorageTables.FOLDER, new QQueryFilter(Db.eq("bucketId", bucketId)), 5000, rows ->
|
||||
{
|
||||
for(QRecord row : rows)
|
||||
{
|
||||
parentOf.put(row.getValueInteger("id"), row.getValueInteger("parentFolderId"));
|
||||
ids.add(row.getValueInteger("id"));
|
||||
}
|
||||
});
|
||||
Map<Integer, long[]> total = new HashMap<>();
|
||||
Map<Integer, Instant> newest = new HashMap<>();
|
||||
for(Map.Entry<Integer, long[]> entry : rollup.direct.entrySet())
|
||||
{
|
||||
Instant when = rollup.newest.get(entry.getKey());
|
||||
for(Integer id = entry.getKey(); id != null; id = parentOf.get(id))
|
||||
{
|
||||
long[] t = total.computeIfAbsent(id, k -> new long[2]);
|
||||
t[0] += entry.getValue()[0];
|
||||
t[1] += entry.getValue()[1];
|
||||
if(when != null && (newest.get(id) == null || when.isAfter(newest.get(id))))
|
||||
{
|
||||
newest.put(id, when);
|
||||
}
|
||||
}
|
||||
}
|
||||
List<QRecord> updates = new ArrayList<>();
|
||||
for(Integer id : ids)
|
||||
{
|
||||
long[] t = total.getOrDefault(id, new long[2]);
|
||||
updates.add(new QRecord().withValue("id", id).withValue("objectCount", t[0]).withValue("totalBytes", t[1]).withValue("newestChange", newest.get(id)));
|
||||
}
|
||||
for(int i = 0; i < updates.size(); i += 1000)
|
||||
{
|
||||
Db.update(ObjectStorageTables.FOLDER, updates.subList(i, Math.min(updates.size(), i + 1000)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Apply a delta to every ancestor folder of a key (write-through), and to the
|
||||
** bucket totals.
|
||||
*******************************************************************************/
|
||||
public static void applyDelta(Integer bucketId, String key, long countDelta, long bytesDelta, Instant newest) throws QException
|
||||
{
|
||||
int slash = key.indexOf('/');
|
||||
while(slash >= 0)
|
||||
{
|
||||
QRecord folder = Db.first(ObjectStorageTables.FOLDER, "bucketId", bucketId, "path", key.substring(0, slash + 1));
|
||||
if(folder != null)
|
||||
{
|
||||
QRecord update = new QRecord().withValue("id", folder.getValue("id"))
|
||||
.withValue("objectCount", Math.max(0, (folder.getValueLong("objectCount") == null ? 0 : folder.getValueLong("objectCount")) + countDelta))
|
||||
.withValue("totalBytes", Math.max(0, (folder.getValueLong("totalBytes") == null ? 0 : folder.getValueLong("totalBytes")) + bytesDelta));
|
||||
if(newest != null && (folder.getValueInstant("newestChange") == null || newest.isAfter(folder.getValueInstant("newestChange"))))
|
||||
{
|
||||
update.withValue("newestChange", newest);
|
||||
}
|
||||
Db.update(ObjectStorageTables.FOLDER, List.of(update));
|
||||
}
|
||||
slash = key.indexOf('/', slash + 1);
|
||||
}
|
||||
QRecord bucket = Db.first(ObjectStorageTables.BUCKET, "id", bucketId);
|
||||
if(bucket != null)
|
||||
{
|
||||
Db.update(ObjectStorageTables.BUCKET, List.of(new QRecord().withValue("id", bucketId)
|
||||
.withValue("objectCount", Math.max(0, (bucket.getValueLong("objectCount") == null ? 0 : bucket.getValueLong("objectCount")) + countDelta))
|
||||
.withValue("totalBytes", Math.max(0, (bucket.getValueLong("totalBytes") == null ? 0 : bucket.getValueLong("totalBytes")) + bytesDelta))));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,636 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.index;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qbits.objectstorage.api.ListRequest;
|
||||
import com.kingsrook.qbits.objectstorage.api.ObjectHeaders;
|
||||
import com.kingsrook.qbits.objectstorage.api.ObjectStorage;
|
||||
import com.kingsrook.qbits.objectstorage.api.ObjectSummary;
|
||||
import com.kingsrook.qbits.objectstorage.api.Page;
|
||||
import com.kingsrook.qbits.objectstorage.api.StorageBucket;
|
||||
import com.kingsrook.qbits.objectstorage.api.StoredObject;
|
||||
import com.kingsrook.qbits.objectstorage.config.BucketRole;
|
||||
import com.kingsrook.qbits.objectstorage.config.StorageBucketConfig;
|
||||
import com.kingsrook.qbits.objectstorage.internal.ConfigSync;
|
||||
import com.kingsrook.qbits.objectstorage.internal.Db;
|
||||
import com.kingsrook.qbits.objectstorage.internal.ObjectStorageRuntime;
|
||||
import com.kingsrook.qbits.objectstorage.model.ObjectStorageTables;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The object index: a real QQQ table (object_storage_object) kept in step with
|
||||
** a bucket, because S3 listing is a forward-only cursor with no server-side
|
||||
** sort, filter, count or page N.
|
||||
**
|
||||
** - Full scan: streams ListObjectsV2 pages, upserts rows in batches, marks each
|
||||
** row with the scan generation, and at the end removes rows not seen, with a
|
||||
** safety stop when more than a configurable share would go (usually a wrong
|
||||
** prefix or credentials). The continuation token is saved on the bucket row
|
||||
** so a large scan resumes after a restart.
|
||||
** - HEAD backlog: objects whose headers are unknown get HEADed under the rate
|
||||
** limit, oldest first.
|
||||
** - Write-through: see ObjectIndexHook. An index failure never fails a write.
|
||||
** - Drift: writes made outside QQQ appear at the next scan.
|
||||
**
|
||||
** Deleted objects of a versioned or trash-enabled bucket are kept as rows with
|
||||
** deleted=true (so Restore can find them) until the trash retention passes.
|
||||
*******************************************************************************/
|
||||
public final class ObjectIndex
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(ObjectIndex.class);
|
||||
|
||||
public static final int PAGE_SIZE = 1000;
|
||||
|
||||
private static final Set<String> RUNNING = java.util.concurrent.ConcurrentHashMap.newKeySet();
|
||||
|
||||
/** Test seam: simulate a crash after this many pages of a scan (so resume can be tested). */
|
||||
static volatile Integer failAfterPages = null;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The outcome of a scan.
|
||||
*******************************************************************************/
|
||||
public record ScanResult(String status, long seen, long added, long updated, long removed, long markedDeleted, int pages, String note)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private ObjectIndex()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether a scan of the bucket is running in this app instance.
|
||||
*******************************************************************************/
|
||||
public static boolean isScanning(String alias)
|
||||
{
|
||||
return (RUNNING.contains(alias));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Scan a bucket (at most one running scan per bucket). Never throws for a
|
||||
** provider error: the result carries the status and the bucket row says so.
|
||||
*******************************************************************************/
|
||||
public static ScanResult scan(String alias) throws QException
|
||||
{
|
||||
StorageBucketConfig config = ObjectStorageRuntime.bucketConfig(alias);
|
||||
if(!Boolean.TRUE.equals(config.getIndexed()))
|
||||
{
|
||||
return (new ScanResult("NOT_INDEXED", 0, 0, 0, 0, 0, 0, "Bucket " + alias + " is not indexed"));
|
||||
}
|
||||
if(!RUNNING.add(alias))
|
||||
{
|
||||
return (new ScanResult("ALREADY_RUNNING", 0, 0, 0, 0, 0, 0, "A scan of " + alias + " is already running"));
|
||||
}
|
||||
try
|
||||
{
|
||||
return (Db.asSystem(() -> doScan(config)));
|
||||
}
|
||||
catch(QException | RuntimeException e)
|
||||
{
|
||||
markBucket(config, "FAILED", null, null);
|
||||
ObjectStorageRuntime.alertSink().raise("storage.scan.stale", "bucket:" + alias, "warning", "The scan of " + alias + " failed: " + e.getMessage(), Map.of("bucket", alias));
|
||||
throw (e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
RUNNING.remove(alias);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The scan itself (runs as the system user).
|
||||
*******************************************************************************/
|
||||
private static ScanResult doScan(StorageBucketConfig config) throws QException
|
||||
{
|
||||
String alias = config.getAlias();
|
||||
Integer bucketId = requireBucketId(alias);
|
||||
QRecord bucket = Db.first(ObjectStorageTables.BUCKET, "id", bucketId);
|
||||
boolean resume = ("RUNNING".equals(bucket.getValueString("lastScanStatus")) || "FAILED".equals(bucket.getValueString("lastScanStatus"))) && StringUtils.hasContent(bucket.getValueString("scanCursor"));
|
||||
long gen = bucket.getValueLong("scanGeneration") == null ? 1 : bucket.getValueLong("scanGeneration") + (resume ? 0 : 1);
|
||||
String token = resume ? bucket.getValueString("scanCursor") : null;
|
||||
long existing = Db.count(ObjectStorageTables.OBJECT, new QQueryFilter(Db.eq("bucketId", bucketId), Db.eq("deleted", false)));
|
||||
|
||||
Db.update(ObjectStorageTables.BUCKET, List.of(new QRecord().withValue("id", bucketId).withValue("lastScanStatus", "RUNNING").withValue("scanGeneration", gen)
|
||||
.withValue("scanCursor", token)));
|
||||
|
||||
StorageBucket storage = ObjectStorage.asSystem("object-storage", "scan-" + alias).bucket(alias);
|
||||
FolderIndex folders = FolderIndex.load(bucketId);
|
||||
long seen = 0;
|
||||
long added = 0;
|
||||
long updated = 0;
|
||||
int pages = 0;
|
||||
boolean capped = false;
|
||||
String fatal = null;
|
||||
Instant now = ObjectStorageRuntime.clock().instant();
|
||||
|
||||
do
|
||||
{
|
||||
Page<ObjectSummary> page;
|
||||
try
|
||||
{
|
||||
page = storage.list(ListRequest.prefix("").pageSize(PAGE_SIZE).after(token));
|
||||
}
|
||||
catch(RuntimeException e)
|
||||
{
|
||||
fatal = e.getMessage();
|
||||
break;
|
||||
}
|
||||
pages++;
|
||||
long[] counts = upsertPage(bucketId, gen, page.getItems(), folders, now);
|
||||
added += counts[0];
|
||||
updated += counts[1];
|
||||
seen += page.getItems().size();
|
||||
token = page.getNextToken();
|
||||
Db.update(ObjectStorageTables.BUCKET, List.of(new QRecord().withValue("id", bucketId).withValue("scanCursor", token)));
|
||||
if(failAfterPages != null && pages >= failAfterPages && token != null)
|
||||
{
|
||||
failAfterPages = null;
|
||||
throw (new IllegalStateException("Simulated crash during a scan"));
|
||||
}
|
||||
if(seen > ObjectStorageRuntime.getConfig().getIndexMaxKeys())
|
||||
{
|
||||
capped = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
while(token != null);
|
||||
|
||||
if(fatal != null)
|
||||
{
|
||||
markBucket(config, "FAILED", gen, null);
|
||||
ObjectStorageRuntime.alertSink().raise("storage.scan.stale", "bucket:" + alias, "warning", "The scan of " + alias + " failed: " + fatal, Map.of("bucket", alias));
|
||||
return (new ScanResult("FAILED", seen, added, updated, 0, 0, pages, fatal));
|
||||
}
|
||||
if(capped)
|
||||
{
|
||||
markBucket(config, "CAP_REACHED", gen, null);
|
||||
return (new ScanResult("CAP_REACHED", seen, added, updated, 0, 0, pages, "More than " + ObjectStorageRuntime.getConfig().getIndexMaxKeys() + " keys: the index stopped at the cap"));
|
||||
}
|
||||
|
||||
// the listing completed: handle rows that were not seen
|
||||
List<Serializable> unseenIds = new ArrayList<>();
|
||||
List<QRecord> toMark = new ArrayList<>();
|
||||
boolean keepDeleted = isSoftDeleteBucket(config);
|
||||
QQueryFilter unseenFilter = new QQueryFilter(Db.eq("bucketId", bucketId), Db.eq("deleted", false), new QFilterCriteria("scanGeneration", QCriteriaOperator.LESS_THAN, gen));
|
||||
Db.forEachPage(ObjectStorageTables.OBJECT, unseenFilter, 5000, rows -> rows.forEach(row -> unseenIds.add(row.getValueInteger("id"))));
|
||||
double share = ObjectStorageRuntime.getConfig().getScanRemovalSafetyShare();
|
||||
if(existing > 20 && unseenIds.size() > existing * share)
|
||||
{
|
||||
String note = "The scan would remove " + unseenIds.size() + " of " + existing + " indexed objects (more than " + Math.round(share * 100)
|
||||
+ "%), which usually means a wrong prefix or credentials. Nothing was removed.";
|
||||
markBucket(config, "STOPPED_SAFETY", gen, null);
|
||||
ObjectStorageRuntime.alertSink().raise("storage.scan.stale", "bucket:" + alias, "warning", note, Map.of("bucket", alias));
|
||||
recomputeStats(config);
|
||||
return (new ScanResult("STOPPED_SAFETY", seen, added, updated, 0, 0, pages, note));
|
||||
}
|
||||
long removed = 0;
|
||||
long marked = 0;
|
||||
if(keepDeleted)
|
||||
{
|
||||
for(Serializable id : unseenIds)
|
||||
{
|
||||
toMark.add(new QRecord().withValue("id", id).withValue("deleted", true).withValue("indexedAt", now));
|
||||
}
|
||||
for(int i = 0; i < toMark.size(); i += PAGE_SIZE)
|
||||
{
|
||||
Db.update(ObjectStorageTables.OBJECT, toMark.subList(i, Math.min(toMark.size(), i + PAGE_SIZE)));
|
||||
}
|
||||
marked = toMark.size();
|
||||
}
|
||||
else
|
||||
{
|
||||
for(int i = 0; i < unseenIds.size(); i += PAGE_SIZE)
|
||||
{
|
||||
Db.delete(ObjectStorageTables.OBJECT, unseenIds.subList(i, Math.min(unseenIds.size(), i + PAGE_SIZE)));
|
||||
}
|
||||
removed = unseenIds.size();
|
||||
}
|
||||
if(keepDeleted)
|
||||
{
|
||||
removed += purgeExpiredDeletedRows(config, bucketId, now);
|
||||
}
|
||||
|
||||
markBucket(config, "OK", gen, now);
|
||||
recomputeStats(config);
|
||||
ObjectStorageRuntime.alertSink().resolve("storage.scan.stale", "bucket:" + alias);
|
||||
return (new ScanResult("OK", seen, added, updated, removed, marked, pages, null));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Upsert one page of listed objects; returns {added, updated}.
|
||||
*******************************************************************************/
|
||||
private static long[] upsertPage(Integer bucketId, long gen, List<ObjectSummary> items, FolderIndex folders, Instant now) throws QException
|
||||
{
|
||||
if(items.isEmpty())
|
||||
{
|
||||
return (new long[] { 0, 0 });
|
||||
}
|
||||
List<Serializable> keys = new ArrayList<>();
|
||||
items.forEach(i -> keys.add(i.getKey()));
|
||||
Map<String, QRecord> existing = new HashMap<>();
|
||||
for(QRecord row : Db.query(ObjectStorageTables.OBJECT, new QQueryFilter(Db.eq("bucketId", bucketId), new QFilterCriteria("objectKey", QCriteriaOperator.IN, keys))))
|
||||
{
|
||||
existing.put(row.getValueString("objectKey"), row);
|
||||
}
|
||||
List<QRecord> inserts = new ArrayList<>();
|
||||
List<QRecord> updates = new ArrayList<>();
|
||||
for(ObjectSummary s : items)
|
||||
{
|
||||
Integer folderId = folders.ensureFor(s.getKey());
|
||||
QRecord row = existing.get(s.getKey());
|
||||
String etag = cleanEtag(s.getEtag());
|
||||
if(row == null)
|
||||
{
|
||||
inserts.add(newRow(bucketId, s.getKey(), folderId, s.getSize(), etag, s.getLastModified(), s.getStorageClass(), gen, now));
|
||||
}
|
||||
else
|
||||
{
|
||||
boolean changed = !java.util.Objects.equals(row.getValueLong("size"), s.getSize()) || !java.util.Objects.equals(row.getValueString("etag"), etag)
|
||||
|| !java.util.Objects.equals(seconds(row.getValueInstant("lastModified")), seconds(s.getLastModified()));
|
||||
QRecord update = new QRecord().withValue("id", row.getValue("id")).withValue("folderId", folderId).withValue("size", s.getSize()).withValue("etag", etag)
|
||||
.withValue("lastModified", s.getLastModified()).withValue("storageClass", s.getStorageClass()).withValue("scanGeneration", gen).withValue("indexedAt", now)
|
||||
.withValue("deleted", false);
|
||||
if(changed)
|
||||
{
|
||||
update.withValue("headersKnown", false);
|
||||
}
|
||||
updates.add(update);
|
||||
}
|
||||
}
|
||||
if(!inserts.isEmpty())
|
||||
{
|
||||
Db.insert(ObjectStorageTables.OBJECT, inserts);
|
||||
}
|
||||
if(!updates.isEmpty())
|
||||
{
|
||||
Db.update(ObjectStorageTables.OBJECT, updates);
|
||||
}
|
||||
return (new long[] { inserts.size(), updates.size() });
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A new object row.
|
||||
*******************************************************************************/
|
||||
static QRecord newRow(Integer bucketId, String key, Integer folderId, Long size, String etag, Instant lastModified, String storageClass, Long gen, Instant now)
|
||||
{
|
||||
String name = key.substring(key.lastIndexOf('/') + 1);
|
||||
int dot = name.lastIndexOf('.');
|
||||
return (new QRecord().withValue("bucketId", bucketId).withValue("folderId", folderId).withValue("objectKey", key).withValue("name", name)
|
||||
.withValue("extension", dot > 0 ? name.substring(dot + 1).toLowerCase() : null).withValue("size", size).withValue("etag", etag).withValue("lastModified", lastModified)
|
||||
.withValue("storageClass", storageClass).withValue("referenceCount", 0).withValue("headersKnown", false).withValue("deleted", false)
|
||||
.withValue("scanGeneration", gen).withValue("indexedAt", now));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** An instant to whole seconds (a listing and a HEAD report different precision).
|
||||
*******************************************************************************/
|
||||
static Instant seconds(Instant instant)
|
||||
{
|
||||
return (instant == null ? null : instant.truncatedTo(java.time.temporal.ChronoUnit.SECONDS));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The ETag without its quotes.
|
||||
*******************************************************************************/
|
||||
static String cleanEtag(String etag)
|
||||
{
|
||||
return (etag == null ? null : etag.replace("\"", ""));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether deleting from the bucket is reversible (versioning or trash), in
|
||||
** which case unseen rows are kept as deleted rows so Restore can find them.
|
||||
*******************************************************************************/
|
||||
static boolean isSoftDeleteBucket(StorageBucketConfig config)
|
||||
{
|
||||
if(Boolean.TRUE.equals(config.getTrashEnabled()))
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
try
|
||||
{
|
||||
return (ObjectStorage.asSystem("object-storage", "index").bucket(config.getAlias()).isVersioned());
|
||||
}
|
||||
catch(RuntimeException e)
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Remove deleted rows older than the trash retention.
|
||||
*******************************************************************************/
|
||||
private static long purgeExpiredDeletedRows(StorageBucketConfig config, Integer bucketId, Instant now) throws QException
|
||||
{
|
||||
Instant cutoff = now.minus(java.time.Duration.ofDays(config.getTrashRetentionDays()));
|
||||
List<Serializable> ids = new ArrayList<>();
|
||||
Db.forEachPage(ObjectStorageTables.OBJECT, new QQueryFilter(Db.eq("bucketId", bucketId), Db.eq("deleted", true), new QFilterCriteria("indexedAt", QCriteriaOperator.LESS_THAN, cutoff)), 5000,
|
||||
rows -> rows.forEach(r -> ids.add(r.getValueInteger("id"))));
|
||||
for(int i = 0; i < ids.size(); i += PAGE_SIZE)
|
||||
{
|
||||
Db.delete(ObjectStorageTables.OBJECT, ids.subList(i, Math.min(ids.size(), i + PAGE_SIZE)));
|
||||
}
|
||||
return (ids.size());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Set the scan status on the bucket row.
|
||||
*******************************************************************************/
|
||||
private static void markBucket(StorageBucketConfig config, String status, Long gen, Instant completedAt)
|
||||
{
|
||||
try
|
||||
{
|
||||
Integer id = ConfigSync.bucketId(config.getAlias());
|
||||
if(id != null)
|
||||
{
|
||||
QRecord update = new QRecord().withValue("id", id).withValue("lastScanStatus", status);
|
||||
if(completedAt != null)
|
||||
{
|
||||
update.withValue("lastScanDate", completedAt).withValue("scanCursor", null);
|
||||
}
|
||||
Db.asSystem(() -> Db.update(ObjectStorageTables.BUCKET, List.of(update)));
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Could not update the scan status", e, logPair("bucket", config.getAlias()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Recompute the folder rollups and the bucket totals and header coverage from
|
||||
** the object rows.
|
||||
*******************************************************************************/
|
||||
public static void recomputeStats(StorageBucketConfig config) throws QException
|
||||
{
|
||||
Integer bucketId = requireBucketId(config.getAlias());
|
||||
Db.asSystem(() ->
|
||||
{
|
||||
FolderIndex.Rollup rollup = new FolderIndex.Rollup();
|
||||
Db.forEachPage(ObjectStorageTables.OBJECT, new QQueryFilter(Db.eq("bucketId", bucketId), Db.eq("deleted", false)), 5000, rows ->
|
||||
{
|
||||
for(QRecord row : rows)
|
||||
{
|
||||
rollup.add(row.getValueInteger("folderId"), row.getValueLong("size"), row.getValueInstant("lastModified"));
|
||||
}
|
||||
});
|
||||
FolderIndex.writeRollups(bucketId, rollup);
|
||||
long known = Db.count(ObjectStorageTables.OBJECT, new QQueryFilter(Db.eq("bucketId", bucketId), Db.eq("deleted", false), Db.eq("headersKnown", true)));
|
||||
long match = Db.count(ObjectStorageTables.OBJECT, new QQueryFilter(Db.eq("bucketId", bucketId), Db.eq("deleted", false), Db.eq("matchesHeaderPolicy", true)));
|
||||
Db.update(ObjectStorageTables.BUCKET, List.of(new QRecord().withValue("id", bucketId).withValue("objectCount", rollup.totalCount).withValue("totalBytes", rollup.totalBytes)
|
||||
.withValue("headersKnownCount", known).withValue("headersMatchingPolicyCount", match)));
|
||||
return (null);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** HEAD the objects whose headers are unknown (oldest first), up to max, and
|
||||
** record their headers and whether they match the header policy. An object
|
||||
** that no longer exists is marked deleted.
|
||||
*******************************************************************************/
|
||||
public static int headBacklog(String alias, int max) throws QException
|
||||
{
|
||||
StorageBucketConfig config = ObjectStorageRuntime.bucketConfig(alias);
|
||||
if(!Boolean.TRUE.equals(config.getIndexed()))
|
||||
{
|
||||
return (0);
|
||||
}
|
||||
Integer bucketId = requireBucketId(alias);
|
||||
return (Db.asSystem(() ->
|
||||
{
|
||||
List<QRecord> todo = new ArrayList<>();
|
||||
QQueryFilter filter = new QQueryFilter(Db.eq("bucketId", bucketId), Db.eq("deleted", false), Db.eq("headersKnown", false));
|
||||
filter.setLimit(max);
|
||||
filter.withOrderBy(new com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy("lastModified"));
|
||||
todo.addAll(Db.query(ObjectStorageTables.OBJECT, filter));
|
||||
StorageBucket storage = ObjectStorage.asSystem("object-storage", "head-" + alias).bucket(alias);
|
||||
List<QRecord> updates = new ArrayList<>();
|
||||
for(QRecord row : todo)
|
||||
{
|
||||
QRecord update = refreshRow(config, storage, row);
|
||||
if(update != null)
|
||||
{
|
||||
updates.add(update);
|
||||
}
|
||||
}
|
||||
for(int i = 0; i < updates.size(); i += PAGE_SIZE)
|
||||
{
|
||||
Db.update(ObjectStorageTables.OBJECT, updates.subList(i, Math.min(updates.size(), i + PAGE_SIZE)));
|
||||
}
|
||||
if(!updates.isEmpty())
|
||||
{
|
||||
recomputeStats(config);
|
||||
}
|
||||
return (updates.size());
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** HEAD the given object rows and refresh them in the index ("Refresh from
|
||||
** bucket"). Returns {refreshed, markedDeleted, unreadable}.
|
||||
*******************************************************************************/
|
||||
public static int[] refresh(String alias, List<QRecord> rows) throws QException
|
||||
{
|
||||
StorageBucketConfig config = ObjectStorageRuntime.bucketConfig(alias);
|
||||
StorageBucket storage = ObjectStorage.asSystem("object-storage", "refresh-" + alias).bucket(alias);
|
||||
return (Db.asSystem(() ->
|
||||
{
|
||||
int[] counts = new int[3];
|
||||
List<QRecord> updates = new ArrayList<>();
|
||||
for(QRecord row : rows)
|
||||
{
|
||||
QRecord update = refreshRow(config, storage, row);
|
||||
if(update == null)
|
||||
{
|
||||
counts[2]++;
|
||||
}
|
||||
else
|
||||
{
|
||||
updates.add(update);
|
||||
counts[Boolean.TRUE.equals(update.getValueBoolean("deleted")) ? 1 : 0]++;
|
||||
}
|
||||
}
|
||||
for(int i = 0; i < updates.size(); i += PAGE_SIZE)
|
||||
{
|
||||
Db.update(ObjectStorageTables.OBJECT, updates.subList(i, Math.min(updates.size(), i + PAGE_SIZE)));
|
||||
}
|
||||
return (counts);
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** HEAD one object row and build the update (null when the HEAD failed).
|
||||
*******************************************************************************/
|
||||
static QRecord refreshRow(StorageBucketConfig config, StorageBucket storage, QRecord row)
|
||||
{
|
||||
String key = row.getValueString("objectKey");
|
||||
try
|
||||
{
|
||||
java.util.Optional<StoredObject> head = storage.head(key);
|
||||
if(head.isEmpty())
|
||||
{
|
||||
return (new QRecord().withValue("id", row.getValue("id")).withValue("deleted", true).withValue("indexedAt", ObjectStorageRuntime.clock().instant()));
|
||||
}
|
||||
return (headersUpdate(config, row.getValue("id"), head.get()));
|
||||
}
|
||||
catch(RuntimeException e)
|
||||
{
|
||||
LOG.warn("Could not HEAD an object to refresh the index", e, logPair("key", key));
|
||||
return (null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The row update for a fully known object (HEAD result or write-through).
|
||||
*******************************************************************************/
|
||||
static QRecord headersUpdate(StorageBucketConfig config, Serializable id, StoredObject o)
|
||||
{
|
||||
ObjectHeaders headers = o.getHeaders() == null ? new ObjectHeaders() : o.getHeaders();
|
||||
QRecord update = new QRecord().withValue("id", id).withValue("size", o.getSize()).withValue("etag", cleanEtag(o.getEtag())).withValue("lastModified", o.getLastModified())
|
||||
.withValue("storageClass", o.getStorageClass()).withValue("contentType", headers.getContentType()).withValue("cacheControl", headers.getCacheControl())
|
||||
.withValue("contentDisposition", headers.getContentDisposition())
|
||||
.withValue("userMetadata", headers.getUserMetadata() == null || headers.getUserMetadata().isEmpty() ? null : JsonUtils.toJson(new LinkedHashMap<>(headers.getUserMetadata())))
|
||||
.withValue("versionId", o.getVersionId()).withValue("headersKnown", true).withValue("deleted", false).withValue("matchesHeaderPolicy", matchesPolicy(config, o.getKey(), headers))
|
||||
.withValue("indexedAt", ObjectStorageRuntime.clock().instant());
|
||||
return (update);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether the observed headers equal what the policy says (no opinion counts as
|
||||
** a match; BACKUP buckets are outside the policy).
|
||||
*******************************************************************************/
|
||||
public static boolean matchesPolicy(StorageBucketConfig config, String key, ObjectHeaders observed)
|
||||
{
|
||||
if(config.getRole() == BucketRole.BACKUP)
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
ObjectHeaders wanted = ObjectStorageRuntime.headerPolicy().headersFor(config, key, observed.getContentType());
|
||||
if(wanted == null || (wanted.getCacheControl() == null && wanted.getContentDisposition() == null))
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
return (wanted.getCacheControl() == null || java.util.Objects.equals(wanted.getCacheControl().trim(), observed.getCacheControl() == null ? null : observed.getCacheControl().trim()))
|
||||
&& (wanted.getContentDisposition() == null || java.util.Objects.equals(wanted.getContentDisposition().trim(), observed.getContentDisposition() == null ? null : observed.getContentDisposition().trim()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The bucket's row id, failing clearly when the tables are not in the context.
|
||||
*******************************************************************************/
|
||||
static Integer requireBucketId(String alias)
|
||||
{
|
||||
Integer id = ConfigSync.bucketId(alias);
|
||||
if(id == null)
|
||||
{
|
||||
throw (new IllegalStateException("The bucket " + alias + " has no row: the storage tables are not available in this context"));
|
||||
}
|
||||
return (id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The distinct aliases of buckets that are indexed.
|
||||
*******************************************************************************/
|
||||
public static List<String> indexedAliases()
|
||||
{
|
||||
List<String> aliases = new ArrayList<>();
|
||||
for(StorageBucketConfig bucket : ObjectStorageRuntime.getConfig().getBuckets())
|
||||
{
|
||||
if(Boolean.TRUE.equals(bucket.getIndexed()))
|
||||
{
|
||||
aliases.add(bucket.getAlias());
|
||||
}
|
||||
}
|
||||
return (aliases);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Keys of a set, for tests.
|
||||
*******************************************************************************/
|
||||
static Set<String> keysOf(List<QRecord> rows)
|
||||
{
|
||||
Set<String> keys = new HashSet<>();
|
||||
rows.forEach(r -> keys.add(r.getValueString("objectKey")));
|
||||
return (keys);
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.index;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.objectstorage.api.StoredObject;
|
||||
import com.kingsrook.qbits.objectstorage.config.StorageBucketConfig;
|
||||
import com.kingsrook.qbits.objectstorage.internal.ConfigSync;
|
||||
import com.kingsrook.qbits.objectstorage.internal.Db;
|
||||
import com.kingsrook.qbits.objectstorage.internal.IndexHook;
|
||||
import com.kingsrook.qbits.objectstorage.internal.ObjectStorageRuntime;
|
||||
import com.kingsrook.qbits.objectstorage.model.ObjectStorageTables;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Write-through: every write made through the service updates the index
|
||||
** immediately (the object row and the rolled-up folder and bucket totals). A
|
||||
** failure here never fails the write; the next scan repairs the index.
|
||||
*******************************************************************************/
|
||||
public class ObjectIndexHook implements IndexHook
|
||||
{
|
||||
/*******************************************************************************
|
||||
** An object was created or changed.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void objectWritten(StorageBucketConfig bucket, StoredObject object)
|
||||
{
|
||||
if(!applies(bucket))
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
Db.asSystem(() ->
|
||||
{
|
||||
Integer bucketId = ObjectIndex.requireBucketId(bucket.getAlias());
|
||||
QRecord existing = Db.first(ObjectStorageTables.OBJECT, "bucketId", bucketId, "objectKey", object.getKey());
|
||||
Instant now = ObjectStorageRuntime.clock().instant();
|
||||
if(existing == null)
|
||||
{
|
||||
Integer folderId = FolderIndex.empty(bucketId).ensureFor(object.getKey());
|
||||
QRecord row = ObjectIndex.newRow(bucketId, object.getKey(), folderId, object.getSize(), ObjectIndex.cleanEtag(object.getEtag()), object.getLastModified(), object.getStorageClass(),
|
||||
currentGeneration(bucketId), now);
|
||||
QRecord inserted = Db.insert(ObjectStorageTables.OBJECT, row);
|
||||
Db.update(ObjectStorageTables.OBJECT, List.of(ObjectIndex.headersUpdate(bucket, inserted.getValue("id"), object)));
|
||||
FolderIndex.applyDelta(bucketId, object.getKey(), 1, object.getSize() == null ? 0 : object.getSize(), object.getLastModified());
|
||||
}
|
||||
else
|
||||
{
|
||||
boolean wasLive = !Boolean.TRUE.equals(existing.getValueBoolean("deleted"));
|
||||
long before = wasLive && existing.getValueLong("size") != null ? existing.getValueLong("size") : 0;
|
||||
Db.update(ObjectStorageTables.OBJECT, List.of(ObjectIndex.headersUpdate(bucket, existing.getValue("id"), object)));
|
||||
FolderIndex.applyDelta(bucketId, object.getKey(), wasLive ? 0 : 1, (object.getSize() == null ? 0 : object.getSize()) - before, object.getLastModified());
|
||||
}
|
||||
return (null);
|
||||
});
|
||||
}
|
||||
catch(QException | RuntimeException e)
|
||||
{
|
||||
throw (new IllegalStateException("Index write-through failed: " + e.getMessage(), e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** An object was removed from the live listing.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void objectRemoved(StorageBucketConfig bucket, String key, boolean soft)
|
||||
{
|
||||
if(!applies(bucket))
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
Db.asSystem(() ->
|
||||
{
|
||||
Integer bucketId = ObjectIndex.requireBucketId(bucket.getAlias());
|
||||
QRecord existing = Db.first(ObjectStorageTables.OBJECT, "bucketId", bucketId, "objectKey", key);
|
||||
if(existing == null)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
boolean wasLive = !Boolean.TRUE.equals(existing.getValueBoolean("deleted"));
|
||||
long size = existing.getValueLong("size") == null ? 0 : existing.getValueLong("size");
|
||||
if(soft)
|
||||
{
|
||||
Db.update(ObjectStorageTables.OBJECT, List.of(new QRecord().withValue("id", existing.getValue("id")).withValue("deleted", true)
|
||||
.withValue("indexedAt", ObjectStorageRuntime.clock().instant())));
|
||||
}
|
||||
else
|
||||
{
|
||||
Db.delete(ObjectStorageTables.OBJECT, List.of(existing.getValueInteger("id")));
|
||||
}
|
||||
if(wasLive)
|
||||
{
|
||||
FolderIndex.applyDelta(bucketId, key, -1, -size, null);
|
||||
}
|
||||
return (null);
|
||||
});
|
||||
}
|
||||
catch(QException | RuntimeException e)
|
||||
{
|
||||
throw (new IllegalStateException("Index write-through failed: " + e.getMessage(), e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether the index covers this bucket and the tables are in the context.
|
||||
*******************************************************************************/
|
||||
private static boolean applies(StorageBucketConfig bucket)
|
||||
{
|
||||
return (Boolean.TRUE.equals(bucket.getIndexed()) && Db.available(ObjectStorageTables.OBJECT) && ConfigSync.bucketId(bucket.getAlias()) != null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The bucket's current scan generation (new rows join it, so the next scan
|
||||
** does not treat them as unseen).
|
||||
*******************************************************************************/
|
||||
private static Long currentGeneration(Integer bucketId) throws QException
|
||||
{
|
||||
QRecord bucket = Db.first(ObjectStorageTables.BUCKET, "id", bucketId);
|
||||
return (bucket == null || bucket.getValueLong("scanGeneration") == null ? 0L : bucket.getValueLong("scanGeneration"));
|
||||
}
|
||||
}
|
||||
@@ -1,297 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.index;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qbits.objectstorage.api.MatchMode;
|
||||
import com.kingsrook.qbits.objectstorage.api.StorageReference;
|
||||
import com.kingsrook.qbits.objectstorage.api.StorageReferenceProvider;
|
||||
import com.kingsrook.qbits.objectstorage.config.BucketRole;
|
||||
import com.kingsrook.qbits.objectstorage.config.StorageBucketConfig;
|
||||
import com.kingsrook.qbits.objectstorage.internal.Db;
|
||||
import com.kingsrook.qbits.objectstorage.internal.ObjectStorageRuntime;
|
||||
import com.kingsrook.qbits.objectstorage.model.ObjectStorageTables;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The reference scan: runs after a bucket scan, rebuilds
|
||||
** object_storage_object_reference from the registered StorageReferenceProviders,
|
||||
** and sets object_storage_object.referenceCount.
|
||||
**
|
||||
** - Orphaned = a live object in a reference-tracked bucket with referenceCount 0,
|
||||
** outside the exempt prefixes.
|
||||
** - Missing = a reference row with no matching object (no key match, or a prefix
|
||||
** with zero objects).
|
||||
** - Referenced objects are skipped by Delete and Move.
|
||||
**
|
||||
** BACKUP buckets are never scanned: backup objects are not app media, so they
|
||||
** are never orphaned.
|
||||
*******************************************************************************/
|
||||
public final class ReferenceScan
|
||||
{
|
||||
/*******************************************************************************
|
||||
** The outcome of a reference scan.
|
||||
*******************************************************************************/
|
||||
public record Result(long references, long missing, long orphaned, long orphanedBytes)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private ReferenceScan()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether the bucket takes part in reference scans.
|
||||
*******************************************************************************/
|
||||
public static boolean applies(StorageBucketConfig bucket)
|
||||
{
|
||||
return (Boolean.TRUE.equals(bucket.getReferenceTracked()) && bucket.getRole() != BucketRole.BACKUP && Boolean.TRUE.equals(bucket.getIndexed()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Run the scan for a bucket alias.
|
||||
*******************************************************************************/
|
||||
public static Result run(String alias) throws QException
|
||||
{
|
||||
StorageBucketConfig config = ObjectStorageRuntime.bucketConfig(alias);
|
||||
if(!applies(config))
|
||||
{
|
||||
return (new Result(0, 0, 0, 0));
|
||||
}
|
||||
Integer bucketId = ObjectIndex.requireBucketId(alias);
|
||||
return (Db.asSystem(() -> doRun(config, bucketId)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The scan itself.
|
||||
*******************************************************************************/
|
||||
private static Result doRun(StorageBucketConfig config, Integer bucketId) throws QException
|
||||
{
|
||||
String alias = config.getAlias();
|
||||
Instant now = ObjectStorageRuntime.clock().instant();
|
||||
|
||||
List<StorageReference> references = new ArrayList<>();
|
||||
for(StorageReferenceProvider provider : ObjectStorageRuntime.getConfig().getReferenceProviders())
|
||||
{
|
||||
provider.references(alias).forEach(references::add);
|
||||
}
|
||||
|
||||
// resolve every reference against the index
|
||||
Map<String, Integer> idByKey = new HashMap<>();
|
||||
List<String> keyRefs = new ArrayList<>();
|
||||
for(StorageReference ref : references)
|
||||
{
|
||||
if(ref.getMatchMode() == MatchMode.KEY)
|
||||
{
|
||||
keyRefs.add(ref.getKeyOrPrefix());
|
||||
}
|
||||
}
|
||||
for(int i = 0; i < keyRefs.size(); i += 500)
|
||||
{
|
||||
List<Serializable> slice = new ArrayList<>(keyRefs.subList(i, Math.min(keyRefs.size(), i + 500)));
|
||||
for(QRecord row : Db.query(ObjectStorageTables.OBJECT, new QQueryFilter(Db.eq("bucketId", bucketId), Db.eq("deleted", false), new QFilterCriteria("objectKey", QCriteriaOperator.IN, slice))))
|
||||
{
|
||||
idByKey.put(row.getValueString("objectKey"), row.getValueInteger("id"));
|
||||
}
|
||||
}
|
||||
|
||||
Map<Integer, Integer> counts = new HashMap<>();
|
||||
List<QRecord> rows = new ArrayList<>();
|
||||
long missing = 0;
|
||||
Map<String, List<Integer>> prefixCache = new HashMap<>();
|
||||
for(StorageReference ref : references)
|
||||
{
|
||||
List<Integer> matched = new ArrayList<>();
|
||||
if(ref.getMatchMode() == MatchMode.KEY)
|
||||
{
|
||||
Integer id = idByKey.get(ref.getKeyOrPrefix());
|
||||
if(id != null)
|
||||
{
|
||||
matched.add(id);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
matched = prefixCache.computeIfAbsent(ref.getKeyOrPrefix(), p -> idsUnder(bucketId, p));
|
||||
}
|
||||
for(Integer id : matched)
|
||||
{
|
||||
counts.merge(id, 1, Integer::sum);
|
||||
}
|
||||
boolean isMissing = matched.isEmpty();
|
||||
if(isMissing)
|
||||
{
|
||||
missing++;
|
||||
}
|
||||
rows.add(new QRecord().withValue("bucketId", bucketId).withValue("objectId", matched.isEmpty() ? null : matched.get(0)).withValue("keyOrPrefix", ref.getKeyOrPrefix())
|
||||
.withValue("matchMode", ref.getMatchMode().name()).withValue("sourceTable", ref.getTableName()).withValue("sourceRecordId", ref.getRecordId())
|
||||
.withValue("sourceField", ref.getFieldName()).withValue("label", ref.getLabel()).withValue("missing", isMissing).withValue("scannedAt", now));
|
||||
}
|
||||
|
||||
// rebuild the reference rows
|
||||
List<Serializable> oldIds = new ArrayList<>();
|
||||
Db.forEachPage(ObjectStorageTables.OBJECT_REFERENCE, new QQueryFilter(Db.eq("bucketId", bucketId)), 5000, old -> old.forEach(r -> oldIds.add(r.getValueInteger("id"))));
|
||||
for(int i = 0; i < oldIds.size(); i += 1000)
|
||||
{
|
||||
Db.delete(ObjectStorageTables.OBJECT_REFERENCE, oldIds.subList(i, Math.min(oldIds.size(), i + 1000)));
|
||||
}
|
||||
for(int i = 0; i < rows.size(); i += 1000)
|
||||
{
|
||||
Db.insert(ObjectStorageTables.OBJECT_REFERENCE, rows.subList(i, Math.min(rows.size(), i + 1000)));
|
||||
}
|
||||
|
||||
// set referenceCount where it changed
|
||||
Set<Integer> seenWithCount = new HashSet<>();
|
||||
List<QRecord> updates = new ArrayList<>();
|
||||
Db.forEachPage(ObjectStorageTables.OBJECT, new QQueryFilter(Db.eq("bucketId", bucketId), new QFilterCriteria("referenceCount", QCriteriaOperator.GREATER_THAN, 0)), 5000, objs ->
|
||||
{
|
||||
for(QRecord obj : objs)
|
||||
{
|
||||
Integer id = obj.getValueInteger("id");
|
||||
seenWithCount.add(id);
|
||||
int wanted = counts.getOrDefault(id, 0);
|
||||
if(obj.getValueInteger("referenceCount") != wanted)
|
||||
{
|
||||
updates.add(new QRecord().withValue("id", id).withValue("referenceCount", wanted));
|
||||
}
|
||||
}
|
||||
});
|
||||
for(Map.Entry<Integer, Integer> entry : counts.entrySet())
|
||||
{
|
||||
if(!seenWithCount.contains(entry.getKey()))
|
||||
{
|
||||
updates.add(new QRecord().withValue("id", entry.getKey()).withValue("referenceCount", entry.getValue()));
|
||||
}
|
||||
}
|
||||
for(int i = 0; i < updates.size(); i += 1000)
|
||||
{
|
||||
Db.update(ObjectStorageTables.OBJECT, updates.subList(i, Math.min(updates.size(), i + 1000)));
|
||||
}
|
||||
|
||||
// orphans
|
||||
long[] orphaned = new long[2];
|
||||
Db.forEachPage(ObjectStorageTables.OBJECT, new QQueryFilter(Db.eq("bucketId", bucketId), Db.eq("deleted", false), Db.eq("referenceCount", 0)), 5000, objs ->
|
||||
{
|
||||
for(QRecord obj : objs)
|
||||
{
|
||||
if(!exempt(config, obj.getValueString("objectKey")))
|
||||
{
|
||||
orphaned[0]++;
|
||||
orphaned[1] += obj.getValueLong("size") == null ? 0 : obj.getValueLong("size");
|
||||
}
|
||||
}
|
||||
});
|
||||
Db.update(ObjectStorageTables.BUCKET, List.of(new QRecord().withValue("id", bucketId).withValue("orphanedCount", orphaned[0]).withValue("orphanedBytes", orphaned[1])
|
||||
.withValue("missingCount", missing).withValue("lastReferenceScanDate", now)));
|
||||
raiseAlerts(config, bucketId, missing, orphaned[0]);
|
||||
return (new Result(references.size(), missing, orphaned[0], orphaned[1]));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The ids of live objects under a prefix.
|
||||
*******************************************************************************/
|
||||
private static List<Integer> idsUnder(Integer bucketId, String prefix)
|
||||
{
|
||||
List<Integer> ids = new ArrayList<>();
|
||||
try
|
||||
{
|
||||
Db.forEachPage(ObjectStorageTables.OBJECT, new QQueryFilter(Db.eq("bucketId", bucketId), Db.eq("deleted", false), new QFilterCriteria("objectKey", QCriteriaOperator.STARTS_WITH, prefix)), 5000,
|
||||
rows -> rows.forEach(r -> ids.add(r.getValueInteger("id"))));
|
||||
}
|
||||
catch(QException e)
|
||||
{
|
||||
throw (new IllegalStateException(e));
|
||||
}
|
||||
return (ids);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether a key is under an exempt prefix (never reported as orphaned).
|
||||
*******************************************************************************/
|
||||
static boolean exempt(StorageBucketConfig config, String key)
|
||||
{
|
||||
for(String prefix : config.getExemptPrefixes())
|
||||
{
|
||||
if(key != null && key.startsWith(prefix))
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
}
|
||||
return (false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Raise or resolve the missing and orphaned alerts (a no-op without the Alerts
|
||||
** qbit).
|
||||
*******************************************************************************/
|
||||
private static void raiseAlerts(StorageBucketConfig config, Integer bucketId, long missing, long orphaned) throws QException
|
||||
{
|
||||
String alias = config.getAlias();
|
||||
if(missing > 0)
|
||||
{
|
||||
ObjectStorageRuntime.alertSink().raise("storage.objects.missing", "bucket:" + alias, "error", missing + " referenced objects are missing from " + alias, Map.of("bucket", alias, "count", missing));
|
||||
}
|
||||
else
|
||||
{
|
||||
ObjectStorageRuntime.alertSink().resolve("storage.objects.missing", "bucket:" + alias);
|
||||
}
|
||||
QRecord bucket = Db.first(ObjectStorageTables.BUCKET, "id", bucketId);
|
||||
Integer threshold = bucket == null ? null : bucket.getValueInteger("orphanCountAlertThreshold");
|
||||
if(threshold != null && orphaned > threshold)
|
||||
{
|
||||
ObjectStorageRuntime.alertSink().raise("storage.objects.orphaned", "bucket:" + alias, "warning", orphaned + " orphaned objects in " + alias, Map.of("bucket", alias, "count", orphaned));
|
||||
}
|
||||
else
|
||||
{
|
||||
ObjectStorageRuntime.alertSink().resolve("storage.objects.orphaned", "bucket:" + alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.index;
|
||||
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.objectstorage.api.ListRequest;
|
||||
import com.kingsrook.qbits.objectstorage.api.ObjectStorage;
|
||||
import com.kingsrook.qbits.objectstorage.api.ObjectSummary;
|
||||
import com.kingsrook.qbits.objectstorage.api.Page;
|
||||
import com.kingsrook.qbits.objectstorage.api.StorageBucket;
|
||||
import com.kingsrook.qbits.objectstorage.config.StorageBucketConfig;
|
||||
import com.kingsrook.qbits.objectstorage.internal.Db;
|
||||
import com.kingsrook.qbits.objectstorage.internal.ObjectStorageRuntime;
|
||||
import com.kingsrook.qbits.objectstorage.model.ObjectStorageTables;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The scheduled work: the nightly scan, HEAD backlog, reference scan, usage
|
||||
** snapshot, trash purge, and the freshness and quota alerts. Each step is
|
||||
** isolated: one failing bucket never stops the others.
|
||||
*******************************************************************************/
|
||||
public final class StorageJobs
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(StorageJobs.class);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private StorageJobs()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Run everything for every indexed bucket; returns one line per step.
|
||||
*******************************************************************************/
|
||||
public static List<String> nightly() throws QException
|
||||
{
|
||||
List<String> log = new ArrayList<>();
|
||||
for(String alias : ObjectIndex.indexedAliases())
|
||||
{
|
||||
try
|
||||
{
|
||||
ObjectIndex.ScanResult scan = ObjectIndex.scan(alias);
|
||||
log.add(alias + " scan: " + scan.status() + " (" + scan.seen() + " seen, " + scan.added() + " new, " + scan.removed() + " removed)");
|
||||
int headed = ObjectIndex.headBacklog(alias, ObjectStorageRuntime.getConfig().getHeadBacklogPerRun());
|
||||
log.add(alias + " HEAD backlog: " + headed + " objects");
|
||||
StorageBucketConfig config = ObjectStorageRuntime.bucketConfig(alias);
|
||||
if(ReferenceScan.applies(config))
|
||||
{
|
||||
ReferenceScan.Result r = ReferenceScan.run(alias);
|
||||
log.add(alias + " references: " + r.references() + ", missing " + r.missing() + ", orphaned " + r.orphaned());
|
||||
}
|
||||
usageSnapshot(alias);
|
||||
}
|
||||
catch(QException | RuntimeException e)
|
||||
{
|
||||
LOG.warn("A nightly storage step failed", e, logPair("bucket", alias));
|
||||
log.add(alias + " FAILED: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
for(StorageBucketConfig bucket : ObjectStorageRuntime.getConfig().getBuckets())
|
||||
{
|
||||
if(Boolean.TRUE.equals(bucket.getTrashEnabled()))
|
||||
{
|
||||
try
|
||||
{
|
||||
log.add(bucket.getAlias() + " trash purge: " + purgeTrash(bucket.getAlias()) + " objects");
|
||||
}
|
||||
catch(RuntimeException e)
|
||||
{
|
||||
log.add(bucket.getAlias() + " trash purge FAILED: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
checkFreshness();
|
||||
return (log);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Record today's usage for a bucket (one row per bucket per day) and raise or
|
||||
** resolve the quota alert.
|
||||
*******************************************************************************/
|
||||
public static void usageSnapshot(String alias) throws QException
|
||||
{
|
||||
Integer bucketId = ObjectIndex.requireBucketId(alias);
|
||||
Db.asSystem(() ->
|
||||
{
|
||||
QRecord bucket = Db.first(ObjectStorageTables.BUCKET, "id", bucketId);
|
||||
Map<String, Map<String, Long>> byPrefix = new LinkedHashMap<>();
|
||||
Db.forEachPage(ObjectStorageTables.FOLDER, new QQueryFilter(Db.eq("bucketId", bucketId)), 5000, rows ->
|
||||
{
|
||||
for(QRecord folder : rows)
|
||||
{
|
||||
if(folder.getValueInteger("parentFolderId") == null)
|
||||
{
|
||||
byPrefix.put(folder.getValueString("path"), Map.of("objects", folder.getValueLong("objectCount") == null ? 0L : folder.getValueLong("objectCount"),
|
||||
"bytes", folder.getValueLong("totalBytes") == null ? 0L : folder.getValueLong("totalBytes")));
|
||||
}
|
||||
}
|
||||
});
|
||||
LocalDate today = LocalDate.ofInstant(ObjectStorageRuntime.clock().instant(), ZoneOffset.UTC);
|
||||
QRecord values = new QRecord().withValue("bucketId", bucketId).withValue("snapshotDate", today).withValue("objectCount", bucket.getValueLong("objectCount"))
|
||||
.withValue("totalBytes", bucket.getValueLong("totalBytes")).withValue("byPrefix", JsonUtils.toJson(byPrefix));
|
||||
QRecord existing = Db.first(ObjectStorageTables.USAGE_DAILY, "bucketId", bucketId, "snapshotDate", today);
|
||||
if(existing == null)
|
||||
{
|
||||
Db.insert(ObjectStorageTables.USAGE_DAILY, values);
|
||||
}
|
||||
else
|
||||
{
|
||||
Db.update(ObjectStorageTables.USAGE_DAILY, List.of(values.withValue("id", existing.getValue("id"))));
|
||||
}
|
||||
Long quota = bucket.getValueLong("quotaBytes");
|
||||
long used = bucket.getValueLong("totalBytes") == null ? 0 : bucket.getValueLong("totalBytes");
|
||||
if(quota != null && quota > 0 && used >= quota * 0.8)
|
||||
{
|
||||
boolean critical = used >= quota * 0.95;
|
||||
ObjectStorageRuntime.alertSink().raise("storage.quota.near", "bucket:" + alias, critical ? "error" : "warning",
|
||||
alias + " uses " + Math.round(used * 100.0 / quota) + "% of its quota", Map.of("bucket", alias, "bytes", used, "quota", quota));
|
||||
}
|
||||
else
|
||||
{
|
||||
ObjectStorageRuntime.alertSink().resolve("storage.quota.near", "bucket:" + alias);
|
||||
}
|
||||
return (null);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Permanently delete trash objects older than the retention (the nightly purge
|
||||
** for stores whose lifecycle rules cannot expire the trash). Returns the count.
|
||||
*******************************************************************************/
|
||||
public static int purgeTrash(String alias)
|
||||
{
|
||||
StorageBucketConfig config = ObjectStorageRuntime.bucketConfig(alias);
|
||||
String trashAlias = config.getTrashBucket() == null ? alias : config.getTrashBucket();
|
||||
StorageBucket trash = ObjectStorage.asSystem("object-storage", "trash-purge").bucket(trashAlias);
|
||||
Instant cutoff = ObjectStorageRuntime.clock().instant().minus(Duration.ofDays(config.getTrashRetentionDays()));
|
||||
List<String> expired = new ArrayList<>();
|
||||
String token = null;
|
||||
do
|
||||
{
|
||||
Page<ObjectSummary> page = trash.list(ListRequest.prefix(config.getTrashPrefix()).pageSize(1000).after(token));
|
||||
for(ObjectSummary o : page.getItems())
|
||||
{
|
||||
if(o.getLastModified() != null && o.getLastModified().isBefore(cutoff))
|
||||
{
|
||||
expired.add(o.getKey());
|
||||
}
|
||||
}
|
||||
token = page.getNextToken();
|
||||
}
|
||||
while(token != null);
|
||||
return (expired.isEmpty() ? 0 : trash.purgeKeys(expired));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Raise storage.scan.stale for indexed buckets not scanned in 48 hours.
|
||||
*******************************************************************************/
|
||||
public static void checkFreshness() throws QException
|
||||
{
|
||||
Instant cutoff = ObjectStorageRuntime.clock().instant().minus(Duration.ofHours(48));
|
||||
for(String alias : ObjectIndex.indexedAliases())
|
||||
{
|
||||
Integer id = com.kingsrook.qbits.objectstorage.internal.ConfigSync.bucketId(alias);
|
||||
if(id == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
QRecord bucket = Db.asSystem(() -> Db.first(ObjectStorageTables.BUCKET, "id", id));
|
||||
Instant last = bucket == null ? null : bucket.getValueInstant("lastScanDate");
|
||||
if(last == null || last.isBefore(cutoff))
|
||||
{
|
||||
ObjectStorageRuntime.alertSink().raise("storage.scan.stale", "bucket:" + alias, "warning", "The index of " + alias + " has not completed a scan in 48 hours", Map.of("bucket", alias));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,712 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.internal;
|
||||
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import com.kingsrook.qbits.objectstorage.api.Capability;
|
||||
import com.kingsrook.qbits.objectstorage.api.CapabilityStatus;
|
||||
import com.kingsrook.qbits.objectstorage.api.PresignedPost;
|
||||
import com.kingsrook.qbits.objectstorage.config.AdminApiType;
|
||||
import com.kingsrook.qbits.objectstorage.config.BucketRole;
|
||||
import com.kingsrook.qbits.objectstorage.config.StorageBucketConfig;
|
||||
import com.kingsrook.qbits.objectstorage.config.StorageConnectionConfig;
|
||||
import com.kingsrook.qbits.objectstorage.model.ObjectStorageTables;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import software.amazon.awssdk.core.sync.RequestBody;
|
||||
import software.amazon.awssdk.services.s3.S3Client;
|
||||
import software.amazon.awssdk.services.s3.model.CompleteMultipartUploadRequest;
|
||||
import software.amazon.awssdk.services.s3.model.CompletedMultipartUpload;
|
||||
import software.amazon.awssdk.services.s3.model.CompletedPart;
|
||||
import software.amazon.awssdk.services.s3.model.CopyObjectRequest;
|
||||
import software.amazon.awssdk.services.s3.model.Delete;
|
||||
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
|
||||
import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest;
|
||||
import software.amazon.awssdk.services.s3.model.DeleteObjectsResponse;
|
||||
import software.amazon.awssdk.services.s3.model.HeadObjectRequest;
|
||||
import software.amazon.awssdk.services.s3.model.ListObjectsV2Request;
|
||||
import software.amazon.awssdk.services.s3.model.ListObjectsV2Response;
|
||||
import software.amazon.awssdk.services.s3.model.ObjectIdentifier;
|
||||
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
|
||||
import software.amazon.awssdk.services.s3.model.S3Exception;
|
||||
import software.amazon.awssdk.services.s3.model.UploadPartRequest;
|
||||
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
|
||||
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The capability probe behind "Test connection". Bucket-setting probes are
|
||||
** read-only Get* calls. Write probes (only when asked and the bucket is not
|
||||
** protected) touch a single .qqq-probe/ key that is always deleted afterwards.
|
||||
** The probe never changes bucket configuration and never throws: every
|
||||
** unexpected result is recorded as UNKNOWN with the detail.
|
||||
*******************************************************************************/
|
||||
public final class CapabilityProbe
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(CapabilityProbe.class);
|
||||
|
||||
public static final String PROBE_PREFIX = ".qqq-probe/";
|
||||
|
||||
private static final int MIN_PART_BYTES = 5 * 1024 * 1024;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The outcome of one connection test.
|
||||
*******************************************************************************/
|
||||
public static class Report
|
||||
{
|
||||
private final Map<String, Map<Capability, CapabilityStatus>> byBucket = new java.util.LinkedHashMap<>();
|
||||
private final Map<String, List<String>> notes = new java.util.LinkedHashMap<>();
|
||||
private final List<String> problems = new ArrayList<>();
|
||||
private Long clockSkewSeconds;
|
||||
private Boolean presignHostReachable;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Per bucket alias, the status of each capability probed.
|
||||
*******************************************************************************/
|
||||
public Map<String, Map<Capability, CapabilityStatus>> getByBucket()
|
||||
{
|
||||
return (byBucket);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Per bucket alias, notes (for example "write probes skipped: object lock").
|
||||
*******************************************************************************/
|
||||
public Map<String, List<String>> getNotes()
|
||||
{
|
||||
return (notes);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Connection-level problems (auth, TLS, DNS, clock).
|
||||
*******************************************************************************/
|
||||
public List<String> getProblems()
|
||||
{
|
||||
return (problems);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for clockSkewSeconds.
|
||||
*******************************************************************************/
|
||||
public Long getClockSkewSeconds()
|
||||
{
|
||||
return (clockSkewSeconds);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for presignHostReachable.
|
||||
*******************************************************************************/
|
||||
public Boolean getPresignHostReachable()
|
||||
{
|
||||
return (presignHostReachable);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** One summary line for a bucket: "8 supported, 3 unsupported, 1 not permitted".
|
||||
*******************************************************************************/
|
||||
public String summaryFor(String alias)
|
||||
{
|
||||
Map<CapabilityStatus, Integer> counts = new EnumMap<>(CapabilityStatus.class);
|
||||
byBucket.getOrDefault(alias, Map.of()).values().forEach(s -> counts.merge(s, 1, Integer::sum));
|
||||
List<String> parts = new ArrayList<>();
|
||||
for(CapabilityStatus status : CapabilityStatus.values())
|
||||
{
|
||||
if(counts.containsKey(status))
|
||||
{
|
||||
parts.add(counts.get(status) + " " + status.name().toLowerCase().replace('_', ' '));
|
||||
}
|
||||
}
|
||||
return (parts.isEmpty() ? "nothing probed" : String.join(", ", parts));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private CapabilityProbe()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether a bucket gets write probes: not protected, and not a BACKUP bucket
|
||||
** with object lock on (a probe object there could not be deleted: SF4).
|
||||
*******************************************************************************/
|
||||
public static String writeProbeSkipReason(StorageBucketConfig bucket, ConnectionRuntime connection)
|
||||
{
|
||||
if(Boolean.TRUE.equals(bucket.getProtectedBucket()))
|
||||
{
|
||||
return ("bucket is protected");
|
||||
}
|
||||
if(bucket.getRole() == BucketRole.BACKUP)
|
||||
{
|
||||
try
|
||||
{
|
||||
connection.client().getObjectLockConfiguration(b -> b.bucket(name(bucket)));
|
||||
return ("write probes skipped: object lock");
|
||||
}
|
||||
catch(RuntimeException e)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
}
|
||||
return (null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Probe every bucket of a connection and record the results.
|
||||
*******************************************************************************/
|
||||
public static Report probeConnection(String connectionName, boolean includeWrite)
|
||||
{
|
||||
details.clear();
|
||||
Report report = new Report();
|
||||
ConnectionRuntime connection = ObjectStorageRuntime.connection(connectionName);
|
||||
StorageConnectionConfig config = connection.getConfig();
|
||||
List<StorageBucketConfig> buckets = new ArrayList<>();
|
||||
for(StorageBucketConfig bucket : ObjectStorageRuntime.getConfig().getBuckets())
|
||||
{
|
||||
if(connectionName.equals(bucket.getConnection()))
|
||||
{
|
||||
buckets.add(bucket);
|
||||
}
|
||||
}
|
||||
|
||||
StorageBucketConfig writable = null;
|
||||
for(StorageBucketConfig bucket : buckets)
|
||||
{
|
||||
Map<Capability, CapabilityStatus> results = new EnumMap<>(Capability.class);
|
||||
report.byBucket.put(bucket.getAlias(), results);
|
||||
List<String> notes = new ArrayList<>();
|
||||
report.notes.put(bucket.getAlias(), notes);
|
||||
|
||||
readProbes(connection, bucket, results, report);
|
||||
recordVersioningState(bucket);
|
||||
|
||||
if(includeWrite)
|
||||
{
|
||||
String skip = writeProbeSkipReason(bucket, connection);
|
||||
if(skip != null)
|
||||
{
|
||||
notes.add(skip.startsWith("write probes skipped") ? skip : "write probes skipped: " + skip);
|
||||
}
|
||||
else
|
||||
{
|
||||
writeProbes(connection, bucket, results);
|
||||
if(writable == null)
|
||||
{
|
||||
writable = bucket;
|
||||
}
|
||||
}
|
||||
}
|
||||
for(Map.Entry<Capability, CapabilityStatus> e : results.entrySet())
|
||||
{
|
||||
CapabilityStore.record(bucket, e.getKey(), e.getValue(), details.getOrDefault(bucket.getAlias() + ":" + e.getKey(), null));
|
||||
}
|
||||
}
|
||||
|
||||
if(includeWrite && writable != null)
|
||||
{
|
||||
Map<Capability, CapabilityStatus> presign = new EnumMap<>(Capability.class);
|
||||
presignProbes(connection, writable, presign, report);
|
||||
report.byBucket.get(writable.getAlias()).putAll(presign);
|
||||
for(Map.Entry<Capability, CapabilityStatus> e : presign.entrySet())
|
||||
{
|
||||
CapabilityStore.record(writable, e.getKey(), e.getValue(), details.get(writable.getAlias() + ":" + e.getKey()));
|
||||
}
|
||||
}
|
||||
|
||||
for(Capability admin : new Capability[] { Capability.USAGE_ADMIN_API, Capability.QUOTA_ADMIN_API })
|
||||
{
|
||||
CapabilityStatus status = config.getAdminApiType() == AdminApiType.NONE ? CapabilityStatus.UNSUPPORTED : CapabilityStatus.UNKNOWN;
|
||||
for(StorageBucketConfig bucket : buckets)
|
||||
{
|
||||
report.byBucket.get(bucket.getAlias()).put(admin, status);
|
||||
}
|
||||
if(!buckets.isEmpty())
|
||||
{
|
||||
CapabilityStore.record(buckets.get(0), admin, status, config.getAdminApiType() == AdminApiType.NONE ? "no admin API configured" : "admin API probing is not part of v1");
|
||||
}
|
||||
}
|
||||
return (report);
|
||||
}
|
||||
|
||||
private static final Map<String, String> details = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Show what the probe saw about versioning on the bucket row (Enabled,
|
||||
** Suspended, Off), so the home and the record do not say "unknown".
|
||||
*******************************************************************************/
|
||||
private static void recordVersioningState(StorageBucketConfig bucket)
|
||||
{
|
||||
String seen = details.get(bucket.getAlias() + ":" + Capability.VERSIONING);
|
||||
if(seen == null || !Db.available(ObjectStorageTables.BUCKET))
|
||||
{
|
||||
return;
|
||||
}
|
||||
String state = seen.equals("Enabled") || seen.equals("Suspended") ? seen : seen.equals("not enabled") ? "Off" : seen.contains("stub") ? "not supported" : null;
|
||||
Integer id = ConfigSync.bucketId(bucket.getAlias());
|
||||
if(state == null || id == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
Db.asSystem(() -> Db.update(ObjectStorageTables.BUCKET, List.of(new QRecord().withValue("id", id).withValue("versioningState", state))));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.debug("Could not record the versioning state", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The read-only probes of one bucket.
|
||||
*******************************************************************************/
|
||||
private static void readProbes(ConnectionRuntime connection, StorageBucketConfig bucket, Map<Capability, CapabilityStatus> results, Report report)
|
||||
{
|
||||
S3Client client = connection.client();
|
||||
String name = name(bucket);
|
||||
String alias = bucket.getAlias();
|
||||
String sampleKey = null;
|
||||
|
||||
try(RateLimiter.Permit ignored = connection.getRateLimiter().acquire())
|
||||
{
|
||||
ListObjectsV2Response r = client.listObjectsV2(ListObjectsV2Request.builder().bucket(name).maxKeys(1).build());
|
||||
results.put(Capability.LIST_V2, CapabilityStatus.SUPPORTED);
|
||||
if(!r.contents().isEmpty())
|
||||
{
|
||||
sampleKey = r.contents().get(0).key();
|
||||
}
|
||||
r.sdkHttpResponse().firstMatchingHeader("Date").ifPresent(d ->
|
||||
{
|
||||
try
|
||||
{
|
||||
long skew = Math.abs(Duration.between(ZonedDateTime.parse(d, DateTimeFormatter.RFC_1123_DATE_TIME).toInstant(), Instant.now()).getSeconds());
|
||||
if(report.clockSkewSeconds == null || skew > report.clockSkewSeconds)
|
||||
{
|
||||
report.clockSkewSeconds = skew;
|
||||
}
|
||||
}
|
||||
catch(RuntimeException e)
|
||||
{
|
||||
LOG.debug("Could not parse the Date header", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch(RuntimeException e)
|
||||
{
|
||||
results.put(Capability.LIST_V2, classify(alias, Capability.LIST_V2, e));
|
||||
report.problems.add(alias + ": listing failed (" + shortMessage(e) + ")");
|
||||
}
|
||||
|
||||
probe(results, alias, Capability.VERSIONING, () ->
|
||||
{
|
||||
String status = client.getBucketVersioning(b -> b.bucket(name)).statusAsString();
|
||||
if(status == null && ProviderDefaults.defaultStatus(connection.getConfig().getProviderType(), Capability.VERSIONING) == CapabilityStatus.UNSUPPORTED)
|
||||
{
|
||||
details.put(alias + ":" + Capability.VERSIONING, "the provider reports versioning off and documents it as a stub");
|
||||
return (CapabilityStatus.UNSUPPORTED);
|
||||
}
|
||||
details.put(alias + ":" + Capability.VERSIONING, status == null ? "not enabled" : status);
|
||||
return (CapabilityStatus.SUPPORTED);
|
||||
});
|
||||
probe(results, alias, Capability.OBJECT_LOCK, () ->
|
||||
{
|
||||
try
|
||||
{
|
||||
client.getObjectLockConfiguration(b -> b.bucket(name));
|
||||
return (CapabilityStatus.SUPPORTED);
|
||||
}
|
||||
catch(S3Exception e)
|
||||
{
|
||||
if("ObjectLockConfigurationNotFoundError".equals(ErrorMapper.errorCode(e)))
|
||||
{
|
||||
details.put(alias + ":" + Capability.OBJECT_LOCK, "object lock is not enabled on this bucket");
|
||||
return (CapabilityStatus.UNSUPPORTED);
|
||||
}
|
||||
throw (e);
|
||||
}
|
||||
});
|
||||
probe(results, alias, Capability.LIFECYCLE, () -> notConfiguredOk(connection, Capability.LIFECYCLE, "NoSuchLifecycleConfiguration", () -> client.getBucketLifecycleConfiguration(b -> b.bucket(name))));
|
||||
probe(results, alias, Capability.BUCKET_POLICY, () -> notConfiguredOk(connection, Capability.BUCKET_POLICY, "NoSuchBucketPolicy", () -> client.getBucketPolicy(b -> b.bucket(name))));
|
||||
probe(results, alias, Capability.CORS, () -> notConfiguredOk(connection, Capability.CORS, "NoSuchCORSConfiguration", () -> client.getBucketCors(b -> b.bucket(name))));
|
||||
probe(results, alias, Capability.ACL, () -> notConfiguredOk(connection, Capability.ACL, "NoSuchACL", () -> client.getBucketAcl(b -> b.bucket(name))));
|
||||
|
||||
String key = sampleKey;
|
||||
if(key == null)
|
||||
{
|
||||
results.put(Capability.OBJECT_TAGGING, CapabilityStatus.UNKNOWN);
|
||||
details.put(alias + ":" + Capability.OBJECT_TAGGING, "no object in the bucket to test");
|
||||
}
|
||||
else
|
||||
{
|
||||
probe(results, alias, Capability.OBJECT_TAGGING, () ->
|
||||
{
|
||||
client.getObjectTagging(b -> b.bucket(name).key(key));
|
||||
return (CapabilityStatus.SUPPORTED);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A bucket-setting call where "not configured" is the documented empty answer.
|
||||
** PARTIAL when the provider's seed says the support is partial.
|
||||
*******************************************************************************/
|
||||
private static CapabilityStatus notConfiguredOk(ConnectionRuntime connection, Capability capability, String notConfiguredCode, Runnable call)
|
||||
{
|
||||
try
|
||||
{
|
||||
call.run();
|
||||
}
|
||||
catch(S3Exception e)
|
||||
{
|
||||
if(!notConfiguredCode.equals(ErrorMapper.errorCode(e)))
|
||||
{
|
||||
throw (e);
|
||||
}
|
||||
}
|
||||
return (ProviderDefaults.defaultStatus(connection.getConfig().getProviderType(), capability) == CapabilityStatus.PARTIAL ? CapabilityStatus.PARTIAL : CapabilityStatus.SUPPORTED);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The write probes of one bucket.
|
||||
*******************************************************************************/
|
||||
private static void writeProbes(ConnectionRuntime connection, StorageBucketConfig bucket, Map<Capability, CapabilityStatus> results)
|
||||
{
|
||||
S3Client client = connection.client();
|
||||
String name = name(bucket);
|
||||
String alias = bucket.getAlias();
|
||||
String base = PROBE_PREFIX + UUID.randomUUID();
|
||||
List<String> cleanup = new ArrayList<>();
|
||||
try
|
||||
{
|
||||
probe(results, alias, Capability.PUT_GET_DELETE, () ->
|
||||
{
|
||||
String key = base + "/pgd";
|
||||
cleanup.add(key);
|
||||
client.putObject(PutObjectRequest.builder().bucket(name).key(key).contentType("text/plain").build(), RequestBody.fromString("probe"));
|
||||
client.headObject(HeadObjectRequest.builder().bucket(name).key(key).build());
|
||||
String body = client.getObjectAsBytes(b -> b.bucket(name).key(key)).asUtf8String();
|
||||
client.deleteObject(DeleteObjectRequest.builder().bucket(name).key(key).build());
|
||||
return ("probe".equals(body) ? CapabilityStatus.SUPPORTED : CapabilityStatus.UNKNOWN);
|
||||
});
|
||||
probe(results, alias, Capability.COPY_REPLACE_METADATA, () ->
|
||||
{
|
||||
String key = base + "/copy";
|
||||
cleanup.add(key);
|
||||
client.putObject(PutObjectRequest.builder().bucket(name).key(key).cacheControl("no-cache").build(), RequestBody.fromString("probe"));
|
||||
client.copyObject(CopyObjectRequest.builder().sourceBucket(name).sourceKey(key).destinationBucket(name).destinationKey(key)
|
||||
.metadataDirective(software.amazon.awssdk.services.s3.model.MetadataDirective.REPLACE).cacheControl("max-age=60").contentType("text/plain").build());
|
||||
String cc = client.headObject(HeadObjectRequest.builder().bucket(name).key(key).build()).cacheControl();
|
||||
return ("max-age=60".equals(cc) ? CapabilityStatus.SUPPORTED : CapabilityStatus.UNSUPPORTED);
|
||||
});
|
||||
probe(results, alias, Capability.BATCH_DELETE, () ->
|
||||
{
|
||||
String k1 = base + "/b1";
|
||||
String k2 = base + "/b2";
|
||||
cleanup.add(k1);
|
||||
cleanup.add(k2);
|
||||
client.putObject(PutObjectRequest.builder().bucket(name).key(k1).build(), RequestBody.fromString("1"));
|
||||
client.putObject(PutObjectRequest.builder().bucket(name).key(k2).build(), RequestBody.fromString("2"));
|
||||
DeleteObjectsResponse r = client.deleteObjects(DeleteObjectsRequest.builder().bucket(name)
|
||||
.delete(Delete.builder().objects(ObjectIdentifier.builder().key(k1).build(), ObjectIdentifier.builder().key(k2).build()).build()).build());
|
||||
return (r.errors().isEmpty() ? CapabilityStatus.SUPPORTED : CapabilityStatus.UNKNOWN);
|
||||
});
|
||||
probe(results, alias, Capability.MULTIPART, () ->
|
||||
{
|
||||
String key = base + "/mp";
|
||||
cleanup.add(key);
|
||||
String uploadId = client.createMultipartUpload(b -> b.bucket(name).key(key)).uploadId();
|
||||
try
|
||||
{
|
||||
String e1 = client.uploadPart(UploadPartRequest.builder().bucket(name).key(key).uploadId(uploadId).partNumber(1).build(), RequestBody.fromBytes(new byte[MIN_PART_BYTES])).eTag();
|
||||
String e2 = client.uploadPart(UploadPartRequest.builder().bucket(name).key(key).uploadId(uploadId).partNumber(2).build(), RequestBody.fromBytes(new byte[1024])).eTag();
|
||||
client.completeMultipartUpload(CompleteMultipartUploadRequest.builder().bucket(name).key(key).uploadId(uploadId).multipartUpload(CompletedMultipartUpload.builder()
|
||||
.parts(CompletedPart.builder().partNumber(1).eTag(e1).build(), CompletedPart.builder().partNumber(2).eTag(e2).build()).build()).build());
|
||||
return (CapabilityStatus.SUPPORTED);
|
||||
}
|
||||
catch(RuntimeException e)
|
||||
{
|
||||
try
|
||||
{
|
||||
client.abortMultipartUpload(b -> b.bucket(name).key(key).uploadId(uploadId));
|
||||
}
|
||||
catch(RuntimeException ignored)
|
||||
{
|
||||
LOG.debug("abort after a failed multipart probe", ignored);
|
||||
}
|
||||
throw (e);
|
||||
}
|
||||
});
|
||||
probe(results, alias, Capability.CONDITIONAL_WRITE, () ->
|
||||
{
|
||||
String key = base + "/cond";
|
||||
cleanup.add(key);
|
||||
client.putObject(PutObjectRequest.builder().bucket(name).key(key).build(), RequestBody.fromString("1"));
|
||||
try
|
||||
{
|
||||
client.putObject(PutObjectRequest.builder().bucket(name).key(key).ifNoneMatch("*").build(), RequestBody.fromString("2"));
|
||||
details.put(alias + ":" + Capability.CONDITIONAL_WRITE, "If-None-Match was ignored");
|
||||
return (CapabilityStatus.UNSUPPORTED);
|
||||
}
|
||||
catch(S3Exception e)
|
||||
{
|
||||
if(e.statusCode() == 412)
|
||||
{
|
||||
return (CapabilityStatus.SUPPORTED);
|
||||
}
|
||||
throw (e);
|
||||
}
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
for(String key : cleanup)
|
||||
{
|
||||
try(RateLimiter.Permit ignored = connection.getRateLimiter().acquire())
|
||||
{
|
||||
client.deleteObject(DeleteObjectRequest.builder().bucket(name).key(key).build());
|
||||
}
|
||||
catch(RuntimeException e)
|
||||
{
|
||||
LOG.warn("Could not delete a probe object", e, logPair("key", key));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The presign probes: presign, then a plain HTTP request to the URL.
|
||||
*******************************************************************************/
|
||||
private static void presignProbes(ConnectionRuntime connection, StorageBucketConfig bucket, Map<Capability, CapabilityStatus> results, Report report)
|
||||
{
|
||||
String name = name(bucket);
|
||||
String alias = bucket.getAlias();
|
||||
String key = PROBE_PREFIX + UUID.randomUUID() + "/presign";
|
||||
HttpClient http = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
|
||||
try
|
||||
{
|
||||
probe(results, alias, Capability.PRESIGNED_PUT, () ->
|
||||
{
|
||||
String url = connection.presigner().presignPutObject(PutObjectPresignRequest.builder().signatureDuration(Duration.ofMinutes(2))
|
||||
.putObjectRequest(b -> b.bucket(name).key(key).contentType("text/plain")).build()).url().toString();
|
||||
HttpResponse<String> r = http.send(HttpRequest.newBuilder(URI.create(url)).header("Content-Type", "text/plain").PUT(HttpRequest.BodyPublishers.ofString("probe")).build(),
|
||||
HttpResponse.BodyHandlers.ofString());
|
||||
report.presignHostReachable = true;
|
||||
return (httpStatus(alias, Capability.PRESIGNED_PUT, r.statusCode(), r.body()));
|
||||
});
|
||||
probe(results, alias, Capability.PRESIGNED_GET, () ->
|
||||
{
|
||||
String url = connection.presigner().presignGetObject(GetObjectPresignRequest.builder().signatureDuration(Duration.ofMinutes(2))
|
||||
.getObjectRequest(b -> b.bucket(name).key(key)).build()).url().toString();
|
||||
HttpResponse<String> r = http.send(HttpRequest.newBuilder(URI.create(url)).GET().build(), HttpResponse.BodyHandlers.ofString());
|
||||
report.presignHostReachable = true;
|
||||
if(r.statusCode() == 200 && !"probe".equals(r.body()))
|
||||
{
|
||||
details.put(alias + ":" + Capability.PRESIGNED_GET, "the presigned GET returned unexpected content (does it reach the same store?)");
|
||||
return (CapabilityStatus.UNKNOWN);
|
||||
}
|
||||
return (httpStatus(alias, Capability.PRESIGNED_GET, r.statusCode(), r.body()));
|
||||
});
|
||||
probe(results, alias, Capability.PRESIGNED_POST, () ->
|
||||
{
|
||||
String[] creds = connection.presignCredentials();
|
||||
PresignedPost post = PostPolicySigner.sign(creds[0], creds[1], creds[2], creds[3], connection.getConfig().effectiveAddressingStyle(), name, key + "-post",
|
||||
null, 0L, 1024L, Instant.now(), Duration.ofMinutes(2));
|
||||
String boundary = "----qqqprobe" + UUID.randomUUID();
|
||||
StringBuilder body = new StringBuilder();
|
||||
for(Map.Entry<String, String> field : post.getFields().entrySet())
|
||||
{
|
||||
body.append("--").append(boundary).append("\r\nContent-Disposition: form-data; name=\"").append(field.getKey()).append("\"\r\n\r\n").append(field.getValue()).append("\r\n");
|
||||
}
|
||||
body.append("--").append(boundary).append("\r\nContent-Disposition: form-data; name=\"file\"; filename=\"probe.txt\"\r\nContent-Type: text/plain\r\n\r\nprobe\r\n--").append(boundary).append("--\r\n");
|
||||
HttpResponse<String> r = http.send(HttpRequest.newBuilder(URI.create(post.getUrl())).header("Content-Type", "multipart/form-data; boundary=" + boundary)
|
||||
.POST(HttpRequest.BodyPublishers.ofString(body.toString(), StandardCharsets.UTF_8)).build(), HttpResponse.BodyHandlers.ofString());
|
||||
return (httpStatus(alias, Capability.PRESIGNED_POST, r.statusCode(), r.body()));
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
for(String k : new String[] { key, key + "-post" })
|
||||
{
|
||||
try(RateLimiter.Permit ignored = connection.getRateLimiter().acquire())
|
||||
{
|
||||
connection.client().deleteObject(DeleteObjectRequest.builder().bucket(name).key(k).build());
|
||||
}
|
||||
catch(RuntimeException e)
|
||||
{
|
||||
LOG.debug("Could not delete a presign probe object", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(results.get(Capability.PRESIGNED_PUT) == CapabilityStatus.UNKNOWN && report.presignHostReachable == null)
|
||||
{
|
||||
report.presignHostReachable = false;
|
||||
report.problems.add(alias + ": the presign host could not be reached");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Map an HTTP status from a presigned request.
|
||||
*******************************************************************************/
|
||||
private static CapabilityStatus httpStatus(String alias, Capability capability, int status, String body)
|
||||
{
|
||||
if(status >= 200 && status < 300)
|
||||
{
|
||||
return (CapabilityStatus.SUPPORTED);
|
||||
}
|
||||
details.put(alias + ":" + capability, "HTTP " + status + (body == null ? "" : " " + body.replaceAll("\\s+", " ").substring(0, Math.min(200, body.length()))));
|
||||
if(status == 403)
|
||||
{
|
||||
return (CapabilityStatus.NOT_PERMITTED);
|
||||
}
|
||||
if(status == 501 || status == 405)
|
||||
{
|
||||
return (CapabilityStatus.UNSUPPORTED);
|
||||
}
|
||||
return (CapabilityStatus.UNKNOWN);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Something a probe does.
|
||||
*******************************************************************************/
|
||||
@FunctionalInterface
|
||||
private interface ProbeCall
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Do it.
|
||||
*******************************************************************************/
|
||||
CapabilityStatus run() throws Exception;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Run one probe, mapping any failure to a status. Never throws.
|
||||
*******************************************************************************/
|
||||
private static void probe(Map<Capability, CapabilityStatus> results, String alias, Capability capability, ProbeCall call)
|
||||
{
|
||||
try
|
||||
{
|
||||
results.put(capability, call.run());
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
results.put(capability, classify(alias, capability, e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Map a failure to a capability status.
|
||||
*******************************************************************************/
|
||||
static CapabilityStatus classify(String alias, Capability capability, Exception e)
|
||||
{
|
||||
String detail = shortMessage(e);
|
||||
details.put(alias + ":" + capability, detail);
|
||||
if(e instanceof S3Exception s3)
|
||||
{
|
||||
String code = ErrorMapper.errorCode(s3);
|
||||
String lower = (code == null ? "" : code.toLowerCase()) + " " + (s3.getMessage() == null ? "" : s3.getMessage().toLowerCase());
|
||||
if(s3.statusCode() == 403 || "AccessDenied".equals(code))
|
||||
{
|
||||
return (CapabilityStatus.NOT_PERMITTED);
|
||||
}
|
||||
if(s3.statusCode() == 501 || s3.statusCode() == 405 || lower.contains("notimplemented") || lower.contains("not implemented") || lower.contains("not supported")
|
||||
|| lower.contains("unsupported"))
|
||||
{
|
||||
return (CapabilityStatus.UNSUPPORTED);
|
||||
}
|
||||
}
|
||||
return (CapabilityStatus.UNKNOWN);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A short description of a failure (no secrets: SDK messages never carry them).
|
||||
*******************************************************************************/
|
||||
private static String shortMessage(Throwable e)
|
||||
{
|
||||
String code = ErrorMapper.errorCode(e);
|
||||
String message = e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage();
|
||||
String text = (code == null ? "" : code + ": ") + message;
|
||||
return (text.length() > 300 ? text.substring(0, 300) : text);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The bucket name on the provider.
|
||||
*******************************************************************************/
|
||||
private static String name(StorageBucketConfig bucket)
|
||||
{
|
||||
return (SecretRefs.resolveSetting(bucket.getBucketName()));
|
||||
}
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.internal;
|
||||
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import com.kingsrook.qbits.objectstorage.api.Capability;
|
||||
import com.kingsrook.qbits.objectstorage.api.CapabilityStatus;
|
||||
import com.kingsrook.qbits.objectstorage.config.StorageBucketConfig;
|
||||
import com.kingsrook.qbits.objectstorage.model.ObjectStorageTables;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Capability rows (per connection and per bucket): reads them (cached), falls
|
||||
** back to the provider defaults, and records probe results. UNKNOWN is treated
|
||||
** as unsupported for writes by the UI.
|
||||
*******************************************************************************/
|
||||
public final class CapabilityStore
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(CapabilityStore.class);
|
||||
|
||||
/** Capabilities that are properties of the connection (the rest are per bucket). */
|
||||
public static final Set<Capability> CONNECTION_SCOPE = EnumSet.of(Capability.LIST_V2, Capability.PRESIGNED_GET, Capability.PRESIGNED_PUT,
|
||||
Capability.PRESIGNED_POST, Capability.USAGE_ADMIN_API, Capability.QUOTA_ADMIN_API);
|
||||
|
||||
private static final Map<String, CapabilityStatus> CACHE = new ConcurrentHashMap<>();
|
||||
private static final Set<String> LOADED = ConcurrentHashMap.newKeySet();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private CapabilityStore()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Drop everything cached.
|
||||
*******************************************************************************/
|
||||
public static void clearCache()
|
||||
{
|
||||
CACHE.clear();
|
||||
LOADED.clear();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The status of a capability for a bucket alias.
|
||||
*******************************************************************************/
|
||||
public static CapabilityStatus status(String bucketAlias, Capability capability)
|
||||
{
|
||||
StorageBucketConfig bucket = ObjectStorageRuntime.bucketConfig(bucketAlias);
|
||||
loadIfNeeded(bucket);
|
||||
String key = scopeKey(bucket, capability);
|
||||
CapabilityStatus found = CACHE.get(key);
|
||||
if(found != null)
|
||||
{
|
||||
return (found);
|
||||
}
|
||||
return (ProviderDefaults.defaultStatus(ObjectStorageRuntime.getConfig().findConnection(bucket.getConnection()).getProviderType(), capability));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether the status allows using the feature (known good).
|
||||
*******************************************************************************/
|
||||
public static boolean usable(CapabilityStatus status)
|
||||
{
|
||||
return (status == CapabilityStatus.SUPPORTED || status == CapabilityStatus.PARTIAL);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether the status rules a feature out (known bad), as opposed to untested.
|
||||
*******************************************************************************/
|
||||
public static boolean ruledOut(CapabilityStatus status)
|
||||
{
|
||||
return (status == CapabilityStatus.UNSUPPORTED || status == CapabilityStatus.NOT_PERMITTED);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Record one probe result (cache and, when the tables are there, the row).
|
||||
*******************************************************************************/
|
||||
public static void record(StorageBucketConfig bucket, Capability capability, CapabilityStatus status, String detail)
|
||||
{
|
||||
CACHE.put(scopeKey(bucket, capability), status);
|
||||
if(!Db.available(ObjectStorageTables.BUCKET_CAPABILITY))
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
boolean connectionScope = CONNECTION_SCOPE.contains(capability);
|
||||
String table = connectionScope ? ObjectStorageTables.CONNECTION_CAPABILITY : ObjectStorageTables.BUCKET_CAPABILITY;
|
||||
String idField = connectionScope ? "connectionId" : "bucketId";
|
||||
Integer id = connectionScope ? ConfigSync.connectionId(bucket.getConnection()) : ConfigSync.bucketId(bucket.getAlias());
|
||||
if(id == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Db.asSystem(() ->
|
||||
{
|
||||
QRecord row = Db.first(table, idField, id, "capability", capability.name());
|
||||
QRecord values = new QRecord().withValue(idField, id).withValue("capability", capability.name()).withValue("status", status.name())
|
||||
.withValue("detail", detail).withValue("source", "PROBE").withValue("probedAt", ObjectStorageRuntime.clock().instant());
|
||||
if(row == null)
|
||||
{
|
||||
Db.insert(table, values);
|
||||
}
|
||||
else
|
||||
{
|
||||
Db.update(table, List.of(values.withValue("id", row.getValue("id"))));
|
||||
}
|
||||
return (null);
|
||||
});
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Could not record a storage capability", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The cache key: connection-scoped capabilities share one key per connection.
|
||||
*******************************************************************************/
|
||||
private static String scopeKey(StorageBucketConfig bucket, Capability capability)
|
||||
{
|
||||
return (CONNECTION_SCOPE.contains(capability) ? "c:" + bucket.getConnection() + ":" + capability : "b:" + bucket.getAlias() + ":" + capability);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Load the persisted rows for a bucket (and its connection) once.
|
||||
*******************************************************************************/
|
||||
private static void loadIfNeeded(StorageBucketConfig bucket)
|
||||
{
|
||||
if(!LOADED.add(bucket.getAlias()) || !Db.available(ObjectStorageTables.BUCKET_CAPABILITY))
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
Integer bucketId = ConfigSync.bucketId(bucket.getAlias());
|
||||
Integer connectionId = ConfigSync.connectionId(bucket.getConnection());
|
||||
Db.asSystem(() ->
|
||||
{
|
||||
if(bucketId != null)
|
||||
{
|
||||
for(QRecord row : Db.query(ObjectStorageTables.BUCKET_CAPABILITY, "bucketId", bucketId))
|
||||
{
|
||||
CACHE.putIfAbsent("b:" + bucket.getAlias() + ":" + row.getValueString("capability"), CapabilityStatus.valueOf(row.getValueString("status")));
|
||||
}
|
||||
}
|
||||
if(connectionId != null)
|
||||
{
|
||||
for(QRecord row : Db.query(ObjectStorageTables.CONNECTION_CAPABILITY, "connectionId", connectionId))
|
||||
{
|
||||
CACHE.putIfAbsent("c:" + bucket.getConnection() + ":" + row.getValueString("capability"), CapabilityStatus.valueOf(row.getValueString("status")));
|
||||
}
|
||||
}
|
||||
return (null);
|
||||
});
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Could not load storage capabilities", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,366 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.internal;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import com.kingsrook.qbits.objectstorage.ObjectStorageQBitConfig;
|
||||
import com.kingsrook.qbits.objectstorage.config.CredentialsMode;
|
||||
import com.kingsrook.qbits.objectstorage.config.StorageBucketConfig;
|
||||
import com.kingsrook.qbits.objectstorage.config.StorageConnectionConfig;
|
||||
import com.kingsrook.qbits.objectstorage.model.ObjectStorageTables;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Upserts one object_storage_connection row and one object_storage_bucket row
|
||||
** per configured entry, so the tables always show the real configuration.
|
||||
** Identity and secret-reference fields come from code and are read-only in the
|
||||
** UI; operational fields (rate limits, timeouts, presign TTLs, thresholds) are
|
||||
** set on insert and then belong to the row.
|
||||
*******************************************************************************/
|
||||
public final class ConfigSync
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(ConfigSync.class);
|
||||
|
||||
private static final Map<String, Integer> BUCKET_IDS = new ConcurrentHashMap<>();
|
||||
private static final Map<String, Integer> CONNECTION_IDS = new ConcurrentHashMap<>();
|
||||
|
||||
private static final Map<String, RowLimits> ROW_LIMITS = new ConcurrentHashMap<>();
|
||||
private static final long ROW_LIMIT_CACHE_MILLIS = 15_000L;
|
||||
|
||||
private static volatile boolean synced = false;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private ConfigSync()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Forget what was synced (tests, reinstall).
|
||||
*******************************************************************************/
|
||||
public static void reset()
|
||||
{
|
||||
synced = false;
|
||||
BUCKET_IDS.clear();
|
||||
CONNECTION_IDS.clear();
|
||||
ROW_LIMITS.clear();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Sync once per process (and per context that has the tables); never throws.
|
||||
*******************************************************************************/
|
||||
public static void ensureSynced()
|
||||
{
|
||||
if(!synced && Db.available(ObjectStorageTables.CONNECTION))
|
||||
{
|
||||
try
|
||||
{
|
||||
sync();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Could not sync the storage configuration to its tables", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Upsert every configured connection and bucket.
|
||||
*******************************************************************************/
|
||||
public static synchronized void sync() throws QException
|
||||
{
|
||||
ObjectStorageQBitConfig config = ObjectStorageRuntime.getConfig();
|
||||
Db.asSystem(() ->
|
||||
{
|
||||
for(StorageConnectionConfig connection : config.getConnections())
|
||||
{
|
||||
CONNECTION_IDS.put(connection.getName(), upsertConnection(connection, config));
|
||||
}
|
||||
for(StorageBucketConfig bucket : config.getBuckets())
|
||||
{
|
||||
BUCKET_IDS.put(bucket.getAlias(), upsertBucket(bucket, config));
|
||||
}
|
||||
return (null);
|
||||
});
|
||||
synced = true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The row id of a bucket alias (syncing first if needed), or null.
|
||||
*******************************************************************************/
|
||||
public static Integer bucketId(String alias)
|
||||
{
|
||||
Integer id = BUCKET_IDS.get(alias);
|
||||
if(id == null)
|
||||
{
|
||||
ensureSynced();
|
||||
id = BUCKET_IDS.get(alias);
|
||||
}
|
||||
return (id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The row id of a connection name (syncing first if needed), or null.
|
||||
*******************************************************************************/
|
||||
public static Integer connectionId(String name)
|
||||
{
|
||||
Integer id = CONNECTION_IDS.get(name);
|
||||
if(id == null)
|
||||
{
|
||||
ensureSynced();
|
||||
id = CONNECTION_IDS.get(name);
|
||||
}
|
||||
return (id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The alias of a bucket row id, or null.
|
||||
*******************************************************************************/
|
||||
public static String aliasOf(Integer bucketId)
|
||||
{
|
||||
for(Map.Entry<String, Integer> entry : BUCKET_IDS.entrySet())
|
||||
{
|
||||
if(entry.getValue().equals(bucketId))
|
||||
{
|
||||
return (entry.getKey());
|
||||
}
|
||||
}
|
||||
return (null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Upsert one connection row.
|
||||
*******************************************************************************/
|
||||
private static Integer upsertConnection(StorageConnectionConfig c, ObjectStorageQBitConfig config) throws QException
|
||||
{
|
||||
boolean accessResolved = c.getCredentialsMode() != CredentialsMode.STATIC_REFS
|
||||
|| (SecretRefs.resolve(c.getAccessKeyRef()) != null && SecretRefs.resolve(c.getSecretKeyRef()) != null);
|
||||
boolean hasIncoming = config.getBuckets().stream().anyMatch(b -> c.getName().equals(b.getConnection())
|
||||
&& b.getRole() != null && (b.getRole().name().equals("INCOMING") || b.getRole().name().equals("PUBLIC")));
|
||||
String warning = !c.hasPresignKey() && (StringUtils.hasContent(c.getPresignEndpoint()) || hasIncoming)
|
||||
? "Presigned URLs are signed with the main key, whose access key ID is visible in every URL. Configure a separate least-privilege presign key." : null;
|
||||
|
||||
QRecord identity = new QRecord()
|
||||
.withValue("name", c.getName()).withValue("source", "CONFIG").withValue("providerType", c.getProviderType().name())
|
||||
.withValue("endpoint", c.getEndpoint()).withValue("presignEndpoint", c.getPresignEndpoint()).withValue("region", c.getRegion())
|
||||
.withValue("addressingStyle", c.effectiveAddressingStyle().name()).withValue("signatureVersion", c.getSignatureVersion())
|
||||
.withValue("checksumMode", c.effectiveChecksumMode().name()).withValue("payloadSigning", c.getPayloadSigning().name())
|
||||
.withValue("tlsVerify", c.getTlsVerify()).withValue("credentialsMode", c.getCredentialsMode().name())
|
||||
.withValue("accessKeyRef", c.getAccessKeyRef()).withValue("secretKeyRef", c.getSecretKeyRef())
|
||||
.withValue("presignAccessKeyRef", c.getPresignAccessKeyRef()).withValue("presignSecretKeyRef", c.getPresignSecretKeyRef())
|
||||
.withValue("credentialsResolved", accessResolved).withValue("presignKeyConfigured", c.hasPresignKey())
|
||||
.withValue("presignWarning", warning).withValue("adminApiType", c.getAdminApiType().name())
|
||||
.withValue("settingsHash", new ConnectionRuntime(c).getSettingsHash());
|
||||
|
||||
QRecord existing = Db.first(ObjectStorageTables.CONNECTION, "name", c.getName());
|
||||
if(existing == null)
|
||||
{
|
||||
identity.withValue("maxOpsPerSecond", c.getMaxOpsPerSecond()).withValue("maxConcurrency", c.getMaxConcurrency())
|
||||
.withValue("connectTimeoutMs", c.getConnectTimeoutMs()).withValue("readTimeoutMs", c.getReadTimeoutMs());
|
||||
return (Db.insert(ObjectStorageTables.CONNECTION, identity).getValueInteger("id"));
|
||||
}
|
||||
identity.withValue("id", existing.getValue("id"));
|
||||
Db.update(ObjectStorageTables.CONNECTION, List.of(identity));
|
||||
return (existing.getValueInteger("id"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Upsert one bucket row.
|
||||
*******************************************************************************/
|
||||
private static Integer upsertBucket(StorageBucketConfig b, ObjectStorageQBitConfig config) throws QException
|
||||
{
|
||||
QRecord identity = new QRecord()
|
||||
.withValue("alias", b.getAlias()).withValue("connectionId", CONNECTION_IDS.get(b.getConnection())).withValue("bucketName", b.getBucketName())
|
||||
.withValue("source", "CONFIG").withValue("role", b.getRole().name()).withValue("publicBaseUrl", b.getPublicBaseUrl())
|
||||
.withValue("indexed", b.getIndexed()).withValue("managedBy", b.getManagedBy().name())
|
||||
.withValue("trashBucketAlias", Boolean.TRUE.equals(b.getTrashEnabled()) ? (b.getTrashBucket() == null ? b.getAlias() : b.getTrashBucket()) : null)
|
||||
.withValue("trashPrefix", Boolean.TRUE.equals(b.getTrashEnabled()) ? b.getTrashPrefix() : null)
|
||||
.withValue("referenceTracked", b.getReferenceTracked());
|
||||
|
||||
QRecord existing = Db.first(ObjectStorageTables.BUCKET, "alias", b.getAlias());
|
||||
if(existing == null)
|
||||
{
|
||||
identity.withValue("maxPresignGetSeconds", b.getMaxPresignGetSeconds()).withValue("maxPresignPutSeconds", b.getMaxPresignPutSeconds());
|
||||
return (Db.insert(ObjectStorageTables.BUCKET, identity).getValueInteger("id"));
|
||||
}
|
||||
identity.withValue("id", existing.getValue("id"));
|
||||
Db.update(ObjectStorageTables.BUCKET, List.of(identity));
|
||||
LOG.debug("Updated a storage bucket row", logPair("alias", b.getAlias()), logPair("at", Instant.now()));
|
||||
return (existing.getValueInteger("id"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Operational overrides edited in the UI (rate limits, timeouts), applied to a
|
||||
** copy of the code config when a connection runtime is first built.
|
||||
*******************************************************************************/
|
||||
public static StorageConnectionConfig withOverrides(StorageConnectionConfig base)
|
||||
{
|
||||
if(!Db.available(ObjectStorageTables.CONNECTION))
|
||||
{
|
||||
return (base);
|
||||
}
|
||||
try
|
||||
{
|
||||
QRecord row = Db.asSystem(() -> Db.first(ObjectStorageTables.CONNECTION, "name", base.getName()));
|
||||
if(row == null)
|
||||
{
|
||||
return (base);
|
||||
}
|
||||
if(row.getValueInteger("maxOpsPerSecond") != null)
|
||||
{
|
||||
base.setMaxOpsPerSecond(row.getValueInteger("maxOpsPerSecond"));
|
||||
}
|
||||
if(row.getValueInteger("maxConcurrency") != null)
|
||||
{
|
||||
base.setMaxConcurrency(row.getValueInteger("maxConcurrency"));
|
||||
}
|
||||
if(row.getValueInteger("connectTimeoutMs") != null)
|
||||
{
|
||||
base.setConnectTimeoutMs(row.getValueInteger("connectTimeoutMs"));
|
||||
}
|
||||
if(row.getValueInteger("readTimeoutMs") != null)
|
||||
{
|
||||
base.setReadTimeoutMs(row.getValueInteger("readTimeoutMs"));
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.debug("Could not read connection overrides", e);
|
||||
}
|
||||
return (base);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** What a bucket row says about presign limits (row values, cached briefly).
|
||||
*******************************************************************************/
|
||||
private record RowLimits(long readAt, Integer get, Integer put)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The longest a presigned download link may last, in seconds: the smaller of
|
||||
** the code config and the bucket row (Settings on object_storage_bucket). The
|
||||
** row can only lower the code limit; a missing row, null or a value below one
|
||||
** second means "no row limit". The config object is never modified.
|
||||
*******************************************************************************/
|
||||
public static int effectiveMaxPresignGetSeconds(StorageBucketConfig bucket)
|
||||
{
|
||||
return (lowest(bucket.getMaxPresignGetSeconds(), rowLimits(bucket.getAlias()).get()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The longest a presigned upload link may last, in seconds (see the GET twin).
|
||||
*******************************************************************************/
|
||||
public static int effectiveMaxPresignPutSeconds(StorageBucketConfig bucket)
|
||||
{
|
||||
return (lowest(bucket.getMaxPresignPutSeconds(), rowLimits(bucket.getAlias()).put()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Forget the cached row limits (after a row edit in a test; they also expire).
|
||||
*******************************************************************************/
|
||||
public static void clearRowLimits()
|
||||
{
|
||||
ROW_LIMITS.clear();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The smaller of the code value (3600 when unset) and a usable row value.
|
||||
*******************************************************************************/
|
||||
private static int lowest(Integer code, Integer row)
|
||||
{
|
||||
int base = code == null ? 3600 : code;
|
||||
return (row != null && row > 0 && row < base ? row : base);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The row's presign limits, read at most every 15 seconds per alias (the way
|
||||
** the header rules are cached); never throws.
|
||||
*******************************************************************************/
|
||||
private static RowLimits rowLimits(String alias)
|
||||
{
|
||||
RowLimits cached = ROW_LIMITS.get(alias);
|
||||
if(cached != null && System.currentTimeMillis() - cached.readAt() < ROW_LIMIT_CACHE_MILLIS)
|
||||
{
|
||||
return (cached);
|
||||
}
|
||||
RowLimits limits = new RowLimits(System.currentTimeMillis(), null, null);
|
||||
if(Db.available(ObjectStorageTables.BUCKET))
|
||||
{
|
||||
try
|
||||
{
|
||||
QRecord row = Db.asSystem(() -> Db.first(ObjectStorageTables.BUCKET, "alias", alias));
|
||||
if(row != null)
|
||||
{
|
||||
limits = new RowLimits(System.currentTimeMillis(), row.getValueInteger("maxPresignGetSeconds"), row.getValueInteger("maxPresignPutSeconds"));
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.debug("Could not read bucket presign limits", e);
|
||||
}
|
||||
}
|
||||
ROW_LIMITS.put(alias, limits);
|
||||
return (limits);
|
||||
}
|
||||
}
|
||||
@@ -1,332 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.internal;
|
||||
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.function.Function;
|
||||
import com.kingsrook.qbits.objectstorage.config.AddressingStyle;
|
||||
import com.kingsrook.qbits.objectstorage.config.CredentialsMode;
|
||||
import com.kingsrook.qbits.objectstorage.config.StorageConnectionConfig;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.module.filesystem.s3.model.metadata.S3BackendMetaData;
|
||||
import com.kingsrook.qqq.backend.module.filesystem.s3.model.metadata.S3ChecksumMode;
|
||||
import com.kingsrook.qqq.backend.module.filesystem.s3.utils.S3ClientFactory;
|
||||
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
|
||||
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
|
||||
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
|
||||
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
|
||||
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
|
||||
import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration;
|
||||
import software.amazon.awssdk.regions.Region;
|
||||
import software.amazon.awssdk.services.s3.DelegatingS3Client;
|
||||
import software.amazon.awssdk.services.s3.S3Client;
|
||||
import software.amazon.awssdk.services.s3.S3Configuration;
|
||||
import software.amazon.awssdk.services.s3.model.S3Request;
|
||||
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** One connection's live state: the settings, one cached SDK v2 S3Client and
|
||||
** S3Presigner, and the rate limiter.
|
||||
**
|
||||
** Core owns S3 client construction. This runtime owns its uncached client and
|
||||
** closes it on reset/reinstall; the presigner remains local.
|
||||
*******************************************************************************/
|
||||
public class ConnectionRuntime
|
||||
{
|
||||
private final StorageConnectionConfig config;
|
||||
private final RateLimiter rateLimiter;
|
||||
private final String settingsHash;
|
||||
|
||||
private S3Client client;
|
||||
private S3Presigner presigner;
|
||||
private String presignAccessKeyId;
|
||||
private String presignSecretKey;
|
||||
private String presignRegion;
|
||||
private String presignHost;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor.
|
||||
*******************************************************************************/
|
||||
public ConnectionRuntime(StorageConnectionConfig config)
|
||||
{
|
||||
this.config = config;
|
||||
this.rateLimiter = new RateLimiter(config.getMaxOpsPerSecond(), config.getMaxConcurrency());
|
||||
this.settingsHash = Integer.toHexString(String.valueOf(config.getEndpoint() + "|" + config.getPresignEndpoint() + "|" + config.getRegion() + "|" + config.effectiveAddressingStyle()
|
||||
+ "|" + config.effectiveChecksumMode() + "|" + config.getTlsVerify() + "|" + config.getCredentialsMode()).hashCode());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for config.
|
||||
*******************************************************************************/
|
||||
public StorageConnectionConfig getConfig()
|
||||
{
|
||||
return (config);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for rateLimiter.
|
||||
*******************************************************************************/
|
||||
public RateLimiter getRateLimiter()
|
||||
{
|
||||
return (rateLimiter);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Display hash of connection settings; runtime reset/reinstall rebuilds clients.
|
||||
*******************************************************************************/
|
||||
public String getSettingsHash()
|
||||
{
|
||||
return (settingsHash);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The (lazily built, cached) S3 client.
|
||||
*******************************************************************************/
|
||||
public synchronized S3Client client()
|
||||
{
|
||||
if(client == null)
|
||||
{
|
||||
client = buildClient();
|
||||
}
|
||||
return (client);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The (lazily built, cached) presigner. Uses the presign endpoint and the
|
||||
** separate least-privilege presign key when configured.
|
||||
*******************************************************************************/
|
||||
public synchronized S3Presigner presigner()
|
||||
{
|
||||
if(presigner == null)
|
||||
{
|
||||
presigner = buildPresigner();
|
||||
}
|
||||
return (presigner);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Credentials used for presigned POST (the presign key when there is one).
|
||||
*******************************************************************************/
|
||||
public synchronized String[] presignCredentials()
|
||||
{
|
||||
presigner();
|
||||
return (new String[] { presignAccessKeyId, presignSecretKey, presignRegion, presignHost });
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Close the client and presigner.
|
||||
*******************************************************************************/
|
||||
public synchronized void close()
|
||||
{
|
||||
if(client != null)
|
||||
{
|
||||
client.close();
|
||||
client = null;
|
||||
}
|
||||
if(presigner != null)
|
||||
{
|
||||
presigner.close();
|
||||
presigner = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The region to sign for.
|
||||
*******************************************************************************/
|
||||
private String effectiveRegion()
|
||||
{
|
||||
String region = SecretRefs.resolveSetting(config.getRegion());
|
||||
if(StringUtils.hasContent(region))
|
||||
{
|
||||
return (region);
|
||||
}
|
||||
return (StringUtils.hasContent(config.getEndpoint()) ? "us-east-1" : null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether path-style addressing applies.
|
||||
*******************************************************************************/
|
||||
private boolean pathStyle()
|
||||
{
|
||||
return (config.effectiveAddressingStyle() == AddressingStyle.PATH);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Build the S3 client from the settings.
|
||||
*******************************************************************************/
|
||||
private S3Client buildClient()
|
||||
{
|
||||
S3BackendMetaData backend = backendMetaData();
|
||||
String token = config.getCredentialsMode() == CredentialsMode.STATIC_REFS ? SecretRefs.resolve(config.getSessionTokenRef()) : null;
|
||||
AwsCredentialsProvider sessionCredentials = null;
|
||||
if(token != null && backend.getAccessKey() != null && backend.getSecretKey() != null)
|
||||
{
|
||||
sessionCredentials = StaticCredentialsProvider.create(AwsSessionCredentials.create(backend.getAccessKey(), backend.getSecretKey(), token));
|
||||
}
|
||||
|
||||
S3Client coreClient = S3ClientFactory.buildClient(backend);
|
||||
return (sessionCredentials == null ? coreClient : new SessionCredentialsClient(coreClient, sessionCredentials));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Resolve references before passing the connection settings to core. No bucket
|
||||
** is mapped: one connection serves multiple buckets. Presigning stays local.
|
||||
*******************************************************************************/
|
||||
S3BackendMetaData backendMetaData()
|
||||
{
|
||||
S3BackendMetaData backend = new S3BackendMetaData()
|
||||
.withEndpoint(SecretRefs.resolveSetting(config.getEndpoint()))
|
||||
.withRegion(effectiveRegion())
|
||||
.withPathStyleAccess(pathStyle())
|
||||
.withChecksumMode(S3ChecksumMode.valueOf(config.effectiveChecksumMode().name()))
|
||||
.withTlsVerify(config.getTlsVerify())
|
||||
.withCaCertificatesPem(SecretRefs.resolve(config.getCaBundleRef()))
|
||||
.withConnectTimeoutMillis(config.getConnectTimeoutMs())
|
||||
.withSocketTimeoutMillis(config.getReadTimeoutMs());
|
||||
if(config.getCredentialsMode() == CredentialsMode.STATIC_REFS)
|
||||
{
|
||||
backend.withAccessKey(SecretRefs.resolve(config.getAccessKeyRef()))
|
||||
.withSecretKey(SecretRefs.resolve(config.getSecretKeyRef()));
|
||||
}
|
||||
return (backend);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Build the presigner.
|
||||
*******************************************************************************/
|
||||
private S3Presigner buildPresigner()
|
||||
{
|
||||
boolean separateKey = config.hasPresignKey();
|
||||
String accessRef = separateKey ? config.getPresignAccessKeyRef() : config.getAccessKeyRef();
|
||||
String secretRef = separateKey ? config.getPresignSecretKeyRef() : config.getSecretKeyRef();
|
||||
String endpoint = SecretRefs.resolveSetting(StringUtils.hasContent(config.getPresignEndpoint()) ? config.getPresignEndpoint() : config.getEndpoint());
|
||||
String region = effectiveRegion();
|
||||
|
||||
S3Presigner.Builder builder = S3Presigner.builder()
|
||||
.credentialsProvider(credentials(accessRef, secretRef))
|
||||
.serviceConfiguration(S3Configuration.builder().pathStyleAccessEnabled(pathStyle()).build());
|
||||
if(StringUtils.hasContent(endpoint))
|
||||
{
|
||||
builder.endpointOverride(URI.create(endpoint));
|
||||
}
|
||||
if(region != null)
|
||||
{
|
||||
builder.region(Region.of(region));
|
||||
}
|
||||
|
||||
presignAccessKeyId = SecretRefs.resolve(accessRef);
|
||||
presignSecretKey = SecretRefs.resolve(secretRef);
|
||||
presignRegion = region == null ? "us-east-1" : region;
|
||||
presignHost = endpoint;
|
||||
return (builder.build());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Static keys from the references, or the SDK default chain.
|
||||
*******************************************************************************/
|
||||
private AwsCredentialsProvider credentials(String accessRef, String secretRef)
|
||||
{
|
||||
if(config.getCredentialsMode() == CredentialsMode.STATIC_REFS)
|
||||
{
|
||||
String accessKey = SecretRefs.resolve(accessRef);
|
||||
String secretKey = SecretRefs.resolve(secretRef);
|
||||
if(accessKey != null && secretKey != null)
|
||||
{
|
||||
String token = SecretRefs.resolve(config.getSessionTokenRef());
|
||||
return (StaticCredentialsProvider.create(token == null ? AwsBasicCredentials.create(accessKey, secretKey)
|
||||
: AwsSessionCredentials.create(accessKey, secretKey, token)));
|
||||
}
|
||||
}
|
||||
return (DefaultCredentialsProvider.builder().build());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** STAND-IN(core=QRun-IO/qqq#1165, removeBy=4.1.0-RC.8)
|
||||
** RC.7 S3BackendMetaData and S3ClientFactory still lack a session token or
|
||||
** credentials-provider hook (verified against the released JAR).
|
||||
** Override request credentials only; core still owns the client and transport.
|
||||
*******************************************************************************/
|
||||
private static class SessionCredentialsClient extends DelegatingS3Client
|
||||
{
|
||||
private final AwsCredentialsProvider credentialsProvider;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Wrap a core-built client with the connection's session credentials.
|
||||
*******************************************************************************/
|
||||
private SessionCredentialsClient(S3Client delegate, AwsCredentialsProvider credentialsProvider)
|
||||
{
|
||||
super(delegate);
|
||||
this.credentialsProvider = credentialsProvider;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Preserve request overrides and the concrete request type while adding the
|
||||
** session token to signing, including streaming and paginated operations.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
@SuppressWarnings("unchecked") // toBuilder().build() retains the concrete request type T.
|
||||
protected <T extends S3Request, ReturnT> ReturnT invokeOperation(T request, Function<T, ReturnT> operation)
|
||||
{
|
||||
AwsRequestOverrideConfiguration.Builder overrides = request.overrideConfiguration()
|
||||
.map(AwsRequestOverrideConfiguration::toBuilder).orElseGet(AwsRequestOverrideConfiguration::builder);
|
||||
T signedRequest = (T) request.toBuilder()
|
||||
.overrideConfiguration(overrides.credentialsProvider(credentialsProvider).build())
|
||||
.build();
|
||||
return (operation.apply(signedRequest));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,287 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.internal;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||
import com.kingsrook.qqq.backend.core.context.CapturedContext;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Small helpers for reading and writing the qbit's own tables as the system
|
||||
** user (the service writes its audit log and index regardless of the calling
|
||||
** user's per-bucket scope; the calling user's permissions were checked first).
|
||||
*******************************************************************************/
|
||||
public final class Db
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Something that may throw.
|
||||
*******************************************************************************/
|
||||
@FunctionalInterface
|
||||
public interface Work<T>
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Do the work.
|
||||
*******************************************************************************/
|
||||
T run() throws Exception;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private Db()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether the instance and the table exist in the current context.
|
||||
*******************************************************************************/
|
||||
public static boolean available(String table)
|
||||
{
|
||||
return (QContext.getQInstance() != null && QContext.getQInstance().getTable(table) != null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Run work in the system-user context, restoring the caller's afterwards.
|
||||
*******************************************************************************/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T asSystem(Work<T> work) throws QException
|
||||
{
|
||||
if(QContext.getQInstance() == null)
|
||||
{
|
||||
throw (new QException("No QQQ context is active"));
|
||||
}
|
||||
CapturedContext original = QContext.capture();
|
||||
Object[] holder = new Object[1];
|
||||
Exception[] error = new Exception[1];
|
||||
try
|
||||
{
|
||||
QContext.init(new CapturedContext(original.qInstance(), systemSession(original.qInstance())));
|
||||
try
|
||||
{
|
||||
holder[0] = work.run();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
error[0] = e;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
QContext.init(original);
|
||||
}
|
||||
if(error[0] != null)
|
||||
{
|
||||
if(error[0] instanceof QException qe)
|
||||
{
|
||||
throw (qe);
|
||||
}
|
||||
if(error[0] instanceof RuntimeException re)
|
||||
{
|
||||
throw (re);
|
||||
}
|
||||
throw (new QException("Storage table access failed", error[0]));
|
||||
}
|
||||
return ((T) holder[0]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The core system-user session (every permission and every all-access
|
||||
** security key of the given instance, read per instance). It must be a
|
||||
** QSystemUserSession: token-based authentication modules (OAuth2, Auth0)
|
||||
** accept that class without a token and reject any other tokenless session,
|
||||
** which made every system read and write fail on such hosts.
|
||||
*******************************************************************************/
|
||||
private static QSession systemSession(QInstance instance)
|
||||
{
|
||||
return (new QSystemUserSession(instance));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Query a table with equals criteria (name, value pairs).
|
||||
*******************************************************************************/
|
||||
public static List<QRecord> query(String table, Object... nameValuePairs) throws QException
|
||||
{
|
||||
QQueryFilter filter = new QQueryFilter();
|
||||
for(int i = 0; i + 1 < nameValuePairs.length; i += 2)
|
||||
{
|
||||
filter.withCriteria(new QFilterCriteria((String) nameValuePairs[i], QCriteriaOperator.EQUALS, (Serializable) nameValuePairs[i + 1]));
|
||||
}
|
||||
return (query(table, filter));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Query a table with a filter.
|
||||
*******************************************************************************/
|
||||
public static List<QRecord> query(String table, QQueryFilter filter) throws QException
|
||||
{
|
||||
return (new QueryAction().execute(new QueryInput(table).withFilter(filter)).getRecords());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Run a query page by page (ordered by id), handing each page to the consumer.
|
||||
** The filter's own skip and limit are ignored.
|
||||
*******************************************************************************/
|
||||
public static void forEachPage(String table, QQueryFilter filter, int pageSize, java.util.function.Consumer<List<QRecord>> consumer) throws QException
|
||||
{
|
||||
int skip = 0;
|
||||
while(true)
|
||||
{
|
||||
QQueryFilter page = filter.clone();
|
||||
page.setOrderBys(new ArrayList<>(List.of(new com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy("id"))));
|
||||
page.setSkip(skip);
|
||||
page.setLimit(pageSize);
|
||||
QueryInput input = new QueryInput(table).withFilter(page);
|
||||
input.setShouldGenerateDisplayValues(false);
|
||||
input.setShouldTranslatePossibleValues(false);
|
||||
List<QRecord> records = new QueryAction().execute(input).getRecords();
|
||||
if(records.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
consumer.accept(records);
|
||||
if(records.size() < pageSize)
|
||||
{
|
||||
return;
|
||||
}
|
||||
skip += pageSize;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Count the records matching a filter.
|
||||
*******************************************************************************/
|
||||
public static int count(String table, QQueryFilter filter) throws QException
|
||||
{
|
||||
return (new com.kingsrook.qqq.backend.core.actions.tables.CountAction().execute(new com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput(table).withFilter(filter)).getCount());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** An equals criteria.
|
||||
*******************************************************************************/
|
||||
public static QFilterCriteria eq(String field, Serializable value)
|
||||
{
|
||||
return (new QFilterCriteria(field, QCriteriaOperator.EQUALS, value));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The first record of an equals query, or null.
|
||||
*******************************************************************************/
|
||||
public static QRecord first(String table, Object... nameValuePairs) throws QException
|
||||
{
|
||||
List<QRecord> records = query(table, nameValuePairs);
|
||||
return (records.isEmpty() ? null : records.get(0));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Insert records, returning them with their ids.
|
||||
*******************************************************************************/
|
||||
public static List<QRecord> insert(String table, List<QRecord> records) throws QException
|
||||
{
|
||||
return (new InsertAction().execute(new InsertInput(table).withRecords(records)).getRecords());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Insert one record.
|
||||
*******************************************************************************/
|
||||
public static QRecord insert(String table, QRecord record) throws QException
|
||||
{
|
||||
List<QRecord> list = new ArrayList<>();
|
||||
list.add(record);
|
||||
return (insert(table, list).get(0));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Update records (each must carry its primary key).
|
||||
*******************************************************************************/
|
||||
public static List<QRecord> update(String table, List<QRecord> records) throws QException
|
||||
{
|
||||
return (new UpdateAction().execute(new UpdateInput(table).withRecords(records)).getRecords());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Delete records by primary key.
|
||||
*******************************************************************************/
|
||||
public static void delete(String table, List<Serializable> ids) throws QException
|
||||
{
|
||||
if(!ids.isEmpty())
|
||||
{
|
||||
new DeleteAction().execute(new DeleteInput(table).withPrimaryKeys(ids));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A string value of a record field, or null.
|
||||
*******************************************************************************/
|
||||
public static String str(QRecord record, String field)
|
||||
{
|
||||
return (record == null ? null : record.getValueString(field));
|
||||
}
|
||||
}
|
||||
-146
@@ -1,146 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.internal;
|
||||
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
import com.kingsrook.qbits.objectstorage.api.ObjectHeaderPolicy;
|
||||
import com.kingsrook.qbits.objectstorage.api.ObjectHeaders;
|
||||
import com.kingsrook.qbits.objectstorage.config.BucketRole;
|
||||
import com.kingsrook.qbits.objectstorage.config.StorageBucketConfig;
|
||||
import com.kingsrook.qbits.objectstorage.model.ObjectStorageTables;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The default header policy: the first enabled object_storage_header_rule (by
|
||||
** ascending priority) whose bucket, key glob or regex, and content-type pattern
|
||||
** match decides the headers. BACKUP buckets are never touched. Rules are
|
||||
** ordinary audited data, cached briefly.
|
||||
*******************************************************************************/
|
||||
public class DefaultObjectHeaderPolicy implements ObjectHeaderPolicy
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(DefaultObjectHeaderPolicy.class);
|
||||
|
||||
private static final long CACHE_MILLIS = 15_000L;
|
||||
|
||||
private static volatile List<QRecord> cachedRules;
|
||||
private static volatile long cachedAt;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Drop cached rules (after the rules change).
|
||||
*******************************************************************************/
|
||||
public static void clearCache()
|
||||
{
|
||||
cachedRules = null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The headers the policy gives an object (empty when no rule matches).
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public ObjectHeaders headersFor(StorageBucketConfig bucket, String key, String contentType)
|
||||
{
|
||||
if(bucket.getRole() == BucketRole.BACKUP)
|
||||
{
|
||||
return (new ObjectHeaders());
|
||||
}
|
||||
Integer bucketId = ConfigSync.bucketId(bucket.getAlias());
|
||||
for(QRecord rule : rules())
|
||||
{
|
||||
Integer ruleBucket = rule.getValueInteger("bucketId");
|
||||
if(ruleBucket != null && !ruleBucket.equals(bucketId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if(matches(rule, key, contentType))
|
||||
{
|
||||
return (new ObjectHeaders().withCacheControl(rule.getValueString("cacheControl")).withContentDisposition(rule.getValueString("contentDisposition")));
|
||||
}
|
||||
}
|
||||
return (new ObjectHeaders());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether a rule matches the key and content type.
|
||||
*******************************************************************************/
|
||||
static boolean matches(QRecord rule, String key, String contentType)
|
||||
{
|
||||
String glob = rule.getValueString("keyGlob");
|
||||
String regex = rule.getValueString("keyRegex");
|
||||
String ct = rule.getValueString("contentTypePattern");
|
||||
if(StringUtils.hasContent(glob) && !Globs.matches(glob, key))
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
if(StringUtils.hasContent(regex) && !Pattern.compile(regex).matcher(key).matches())
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
if(StringUtils.hasContent(ct) && (contentType == null || !Globs.matches(ct, contentType)))
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
return (true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The enabled rules by ascending priority (empty when the table is not there).
|
||||
*******************************************************************************/
|
||||
private static List<QRecord> rules()
|
||||
{
|
||||
List<QRecord> rules = cachedRules;
|
||||
if(rules != null && System.currentTimeMillis() - cachedAt < CACHE_MILLIS)
|
||||
{
|
||||
return (rules);
|
||||
}
|
||||
if(!Db.available(ObjectStorageTables.HEADER_RULE))
|
||||
{
|
||||
return (List.of());
|
||||
}
|
||||
try
|
||||
{
|
||||
rules = Db.asSystem(() -> Db.query(ObjectStorageTables.HEADER_RULE, "enabled", true));
|
||||
rules.sort(Comparator.comparing((QRecord r) -> r.getValueInteger("priority") == null ? 100 : r.getValueInteger("priority"))
|
||||
.thenComparing(r -> r.getValueInteger("id") == null ? 0 : r.getValueInteger("id")));
|
||||
cachedRules = rules;
|
||||
cachedAt = System.currentTimeMillis();
|
||||
return (rules);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Could not read storage header rules", e);
|
||||
return (List.of());
|
||||
}
|
||||
}
|
||||
}
|
||||
-102
@@ -1,102 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.internal;
|
||||
|
||||
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qbits.objectstorage.api.BucketKey;
|
||||
import com.kingsrook.qbits.objectstorage.api.PublicUrlResolver;
|
||||
import com.kingsrook.qbits.objectstorage.api.StorageKeys;
|
||||
import com.kingsrook.qbits.objectstorage.config.StorageBucketConfig;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** publicBaseUrl + "/" + percent-encoded key, and the reverse.
|
||||
*******************************************************************************/
|
||||
public class DefaultPublicUrlResolver implements PublicUrlResolver
|
||||
{
|
||||
/*******************************************************************************
|
||||
** The public URL of a key, or null when the bucket has no public base URL.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String resolve(StorageBucketConfig bucket, String key)
|
||||
{
|
||||
String base = SecretRefs.resolveSetting(bucket.getPublicBaseUrl());
|
||||
if(!StringUtils.hasContent(base))
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
return (stripTrailingSlash(base) + "/" + StorageKeys.encodeForUrl(key));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The bucket and key a public URL points at, when one configured base URL
|
||||
** (longest match) is a prefix of it.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Optional<BucketKey> parse(String url)
|
||||
{
|
||||
if(!StringUtils.hasContent(url) || !ObjectStorageRuntime.isInstalled())
|
||||
{
|
||||
return (Optional.empty());
|
||||
}
|
||||
StorageBucketConfig best = null;
|
||||
String bestBase = "";
|
||||
for(StorageBucketConfig bucket : ObjectStorageRuntime.getConfig().getBuckets())
|
||||
{
|
||||
String base = stripTrailingSlash(SecretRefs.resolveSetting(bucket.getPublicBaseUrl()) == null ? "" : SecretRefs.resolveSetting(bucket.getPublicBaseUrl()));
|
||||
if(!base.isEmpty() && url.startsWith(base + "/") && base.length() > bestBase.length())
|
||||
{
|
||||
best = bucket;
|
||||
bestBase = base;
|
||||
}
|
||||
}
|
||||
if(best == null)
|
||||
{
|
||||
return (Optional.empty());
|
||||
}
|
||||
String rest = url.substring(bestBase.length() + 1);
|
||||
int q = rest.indexOf('?');
|
||||
if(q >= 0)
|
||||
{
|
||||
rest = rest.substring(0, q);
|
||||
}
|
||||
return (Optional.of(new BucketKey(best.getAlias(), StorageKeys.decodeFromUrl(rest))));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Remove trailing slashes.
|
||||
*******************************************************************************/
|
||||
private static String stripTrailingSlash(String s)
|
||||
{
|
||||
String r = s;
|
||||
while(r.endsWith("/"))
|
||||
{
|
||||
r = r.substring(0, r.length() - 1);
|
||||
}
|
||||
return (r);
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.internal;
|
||||
|
||||
|
||||
import com.kingsrook.qbits.objectstorage.api.StorageException;
|
||||
import com.kingsrook.qbits.objectstorage.api.StorageException.Reason;
|
||||
import software.amazon.awssdk.awscore.exception.AwsServiceException;
|
||||
import software.amazon.awssdk.core.exception.SdkClientException;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Maps SDK exceptions to the stable StorageException reasons.
|
||||
*******************************************************************************/
|
||||
public final class ErrorMapper
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private ErrorMapper()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The error code the provider returned, or null.
|
||||
*******************************************************************************/
|
||||
public static String errorCode(Throwable t)
|
||||
{
|
||||
if(t instanceof AwsServiceException ase && ase.awsErrorDetails() != null)
|
||||
{
|
||||
return (ase.awsErrorDetails().errorCode());
|
||||
}
|
||||
return (null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Map any throwable.
|
||||
*******************************************************************************/
|
||||
public static StorageException map(Throwable t, String what)
|
||||
{
|
||||
if(t instanceof StorageException se)
|
||||
{
|
||||
return (se);
|
||||
}
|
||||
if(t instanceof AwsServiceException ase)
|
||||
{
|
||||
int status = ase.statusCode();
|
||||
String code = errorCode(ase);
|
||||
String requestId = ase.requestId();
|
||||
Reason reason = Reason.PROVIDER_ERROR;
|
||||
if(status == 404 || "NoSuchKey".equals(code) || "NoSuchBucket".equals(code) || "NotFound".equals(code))
|
||||
{
|
||||
reason = Reason.NOT_FOUND;
|
||||
}
|
||||
else if(status == 403 || status == 401 || "AccessDenied".equals(code) || "InvalidAccessKeyId".equals(code) || "SignatureDoesNotMatch".equals(code))
|
||||
{
|
||||
reason = Reason.ACCESS_DENIED;
|
||||
}
|
||||
else if(status == 412 || "PreconditionFailed".equals(code))
|
||||
{
|
||||
reason = Reason.PRECONDITION_FAILED;
|
||||
}
|
||||
else if(status == 503 || "SlowDown".equals(code) || "Throttling".equals(code) || status == 429)
|
||||
{
|
||||
reason = Reason.THROTTLED;
|
||||
}
|
||||
else if(status == 501 || "NotImplemented".equals(code) || "MethodNotAllowed".equals(code) || status == 405)
|
||||
{
|
||||
reason = Reason.UNSUPPORTED;
|
||||
}
|
||||
return (new StorageException(reason, what + " failed: " + (code == null ? "HTTP " + status : code), requestId, status, ase));
|
||||
}
|
||||
if(t instanceof SdkClientException)
|
||||
{
|
||||
return (new StorageException(Reason.PROVIDER_ERROR, what + " failed: " + t.getMessage(), null, null, t));
|
||||
}
|
||||
return (new StorageException(Reason.PROVIDER_ERROR, what + " failed: " + t, null, null, t));
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.internal;
|
||||
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Glob matching for keys: * matches any run of characters (slashes included),
|
||||
** ? matches one character.
|
||||
*******************************************************************************/
|
||||
public final class Globs
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private Globs()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether a glob matches a whole string.
|
||||
*******************************************************************************/
|
||||
public static boolean matches(String glob, String value)
|
||||
{
|
||||
return (toPattern(glob).matcher(value == null ? "" : value).matches());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Compile a glob.
|
||||
*******************************************************************************/
|
||||
public static Pattern toPattern(String glob)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for(char c : glob.toCharArray())
|
||||
{
|
||||
if(c == '*')
|
||||
{
|
||||
sb.append(".*");
|
||||
}
|
||||
else if(c == '?')
|
||||
{
|
||||
sb.append('.');
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append(Pattern.quote(String.valueOf(c)));
|
||||
}
|
||||
}
|
||||
return (Pattern.compile(sb.toString(), Pattern.DOTALL));
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.internal;
|
||||
|
||||
|
||||
import com.kingsrook.qbits.objectstorage.api.StoredObject;
|
||||
import com.kingsrook.qbits.objectstorage.config.StorageBucketConfig;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Keeps the object index in step with writes made through the service
|
||||
** (write-through). An index failure never fails the write; the next scan
|
||||
** repairs it.
|
||||
*******************************************************************************/
|
||||
public interface IndexHook
|
||||
{
|
||||
/*******************************************************************************
|
||||
** An object was created or changed (the HEAD result after the write).
|
||||
*******************************************************************************/
|
||||
void objectWritten(StorageBucketConfig bucket, StoredObject object);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** An object was removed from the live listing. When soft is true the delete is
|
||||
** reversible (delete marker or trash), so the row is kept as deleted.
|
||||
*******************************************************************************/
|
||||
void objectRemoved(StorageBucketConfig bucket, String key, boolean soft);
|
||||
}
|
||||
@@ -1,298 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.internal;
|
||||
|
||||
|
||||
import java.time.Clock;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import com.kingsrook.qbits.objectstorage.ObjectStorageQBitConfig;
|
||||
import com.kingsrook.qbits.objectstorage.api.ObjectHeaderPolicy;
|
||||
import com.kingsrook.qbits.objectstorage.api.PublicUrlResolver;
|
||||
import com.kingsrook.qbits.objectstorage.api.StorageAlertSink;
|
||||
import com.kingsrook.qbits.objectstorage.api.StorageChangeListener;
|
||||
import com.kingsrook.qbits.objectstorage.config.StorageBucketConfig;
|
||||
import com.kingsrook.qbits.objectstorage.config.StorageConnectionConfig;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Process-wide state of the qbit: the installed config, one ConnectionRuntime
|
||||
** per connection (clients are cached and rebuilt when settings change), the
|
||||
** listeners, the header policy, the URL resolver, the alert sink and a clock
|
||||
** (replaceable in tests).
|
||||
*******************************************************************************/
|
||||
public final class ObjectStorageRuntime
|
||||
{
|
||||
private static final Map<String, ConnectionRuntime> CONNECTIONS = new ConcurrentHashMap<>();
|
||||
private static final List<StorageChangeListener> LISTENERS = new CopyOnWriteArrayList<>();
|
||||
private static final Map<String, String> PROCESS_PERMISSIONS = new ConcurrentHashMap<>();
|
||||
|
||||
private static volatile ObjectStorageQBitConfig config;
|
||||
private static volatile ObjectHeaderPolicy headerPolicy;
|
||||
private static volatile PublicUrlResolver publicUrlResolver;
|
||||
private static volatile StorageAlertSink alertSink = StorageAlertSink.noop();
|
||||
private static volatile Clock clock = Clock.systemUTC();
|
||||
private static volatile IndexHook indexHook;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private ObjectStorageRuntime()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Install the config (called by the producer). Connections are rebuilt.
|
||||
*******************************************************************************/
|
||||
public static synchronized void install(ObjectStorageQBitConfig newConfig)
|
||||
{
|
||||
closeConnections();
|
||||
config = newConfig;
|
||||
headerPolicy = newConfig.getHeaderPolicy() != null ? newConfig.getHeaderPolicy() : new DefaultObjectHeaderPolicy();
|
||||
publicUrlResolver = newConfig.getPublicUrlResolver() != null ? newConfig.getPublicUrlResolver() : new DefaultPublicUrlResolver();
|
||||
alertSink = newConfig.getAlertSink() != null ? newConfig.getAlertSink() : StorageAlertSink.noop();
|
||||
CapabilityStore.clearCache();
|
||||
indexHook = new com.kingsrook.qbits.objectstorage.index.ObjectIndexHook();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Forget everything (tests).
|
||||
*******************************************************************************/
|
||||
public static synchronized void reset()
|
||||
{
|
||||
closeConnections();
|
||||
LISTENERS.clear();
|
||||
PROCESS_PERMISSIONS.clear();
|
||||
config = null;
|
||||
headerPolicy = null;
|
||||
publicUrlResolver = null;
|
||||
alertSink = StorageAlertSink.noop();
|
||||
clock = Clock.systemUTC();
|
||||
indexHook = null;
|
||||
CapabilityStore.clearCache();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Close and drop every cached client.
|
||||
*******************************************************************************/
|
||||
private static void closeConnections()
|
||||
{
|
||||
for(ConnectionRuntime connection : CONNECTIONS.values())
|
||||
{
|
||||
connection.close();
|
||||
}
|
||||
CONNECTIONS.clear();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The installed config; fails clearly when the qbit is not installed.
|
||||
*******************************************************************************/
|
||||
public static ObjectStorageQBitConfig getConfig()
|
||||
{
|
||||
if(config == null)
|
||||
{
|
||||
throw (new IllegalStateException("The Object Storage qbit is not installed"));
|
||||
}
|
||||
return (config);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether the qbit is installed.
|
||||
*******************************************************************************/
|
||||
public static boolean isInstalled()
|
||||
{
|
||||
return (config != null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The (cached) runtime of a connection.
|
||||
*******************************************************************************/
|
||||
public static ConnectionRuntime connection(String name)
|
||||
{
|
||||
StorageConnectionConfig connectionConfig = getConfig().findConnection(name);
|
||||
if(connectionConfig == null)
|
||||
{
|
||||
throw (new IllegalArgumentException("Unknown storage connection: " + name));
|
||||
}
|
||||
return (CONNECTIONS.computeIfAbsent(name, n -> new ConnectionRuntime(ConfigSync.withOverrides(connectionConfig))));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The configured bucket of an alias; fails clearly when unknown.
|
||||
*******************************************************************************/
|
||||
public static StorageBucketConfig bucketConfig(String alias)
|
||||
{
|
||||
StorageBucketConfig bucket = getConfig().findBucket(alias);
|
||||
if(bucket == null)
|
||||
{
|
||||
throw (new IllegalArgumentException("Unknown storage bucket alias: " + alias));
|
||||
}
|
||||
return (bucket);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Register a change listener.
|
||||
*******************************************************************************/
|
||||
public static void addChangeListener(StorageChangeListener listener)
|
||||
{
|
||||
LISTENERS.add(listener);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Remove a change listener.
|
||||
*******************************************************************************/
|
||||
public static void removeChangeListener(StorageChangeListener listener)
|
||||
{
|
||||
LISTENERS.remove(listener);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The registered listeners.
|
||||
*******************************************************************************/
|
||||
public static List<StorageChangeListener> listeners()
|
||||
{
|
||||
return (LISTENERS);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The header policy in force.
|
||||
*******************************************************************************/
|
||||
public static ObjectHeaderPolicy headerPolicy()
|
||||
{
|
||||
return (headerPolicy == null ? new DefaultObjectHeaderPolicy() : headerPolicy);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The public URL resolver in force.
|
||||
*******************************************************************************/
|
||||
public static PublicUrlResolver publicUrlResolver()
|
||||
{
|
||||
return (publicUrlResolver == null ? new DefaultPublicUrlResolver() : publicUrlResolver);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Replace the public URL resolver (the CDN qbit does this).
|
||||
*******************************************************************************/
|
||||
public static void setPublicUrlResolver(PublicUrlResolver resolver)
|
||||
{
|
||||
publicUrlResolver = resolver;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The alert sink.
|
||||
*******************************************************************************/
|
||||
public static StorageAlertSink alertSink()
|
||||
{
|
||||
return (alertSink);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Replace the alert sink (an Alerts qbit adapter does this).
|
||||
*******************************************************************************/
|
||||
public static void setAlertSink(StorageAlertSink sink)
|
||||
{
|
||||
alertSink = sink == null ? StorageAlertSink.noop() : sink;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The clock.
|
||||
*******************************************************************************/
|
||||
public static Clock clock()
|
||||
{
|
||||
return (clock);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Replace the clock (tests).
|
||||
*******************************************************************************/
|
||||
public static void setClock(Clock newClock)
|
||||
{
|
||||
clock = newClock;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The write-through index hook, or null when the index is not active.
|
||||
*******************************************************************************/
|
||||
public static IndexHook indexHook()
|
||||
{
|
||||
return (indexHook);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Set the write-through index hook.
|
||||
*******************************************************************************/
|
||||
public static void setIndexHook(IndexHook hook)
|
||||
{
|
||||
indexHook = hook;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The permission each process needs (default object_storage.admin).
|
||||
*******************************************************************************/
|
||||
public static Map<String, String> processPermissions()
|
||||
{
|
||||
return (PROCESS_PERMISSIONS);
|
||||
}
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.internal;
|
||||
|
||||
|
||||
import com.kingsrook.qbits.objectstorage.api.StorageCaller;
|
||||
import com.kingsrook.qbits.objectstorage.model.ObjectStorageTables;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Writes object_storage_operation: the storage audit log. Every write from
|
||||
** every caller (UI action, app code, scheduled job) lands here. Presigned URLs
|
||||
** are logged without the signature (key, method, expiry and issuer only),
|
||||
** because the URL itself is a bearer token. A logging failure never fails the
|
||||
** operation it describes.
|
||||
*******************************************************************************/
|
||||
public final class OperationLog
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(OperationLog.class);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private OperationLog()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** One log entry under construction.
|
||||
*******************************************************************************/
|
||||
public static class Entry
|
||||
{
|
||||
private final StorageCaller caller;
|
||||
private final String action;
|
||||
private final String bucketAlias;
|
||||
private String key;
|
||||
private String destinationKey;
|
||||
private Long bytes;
|
||||
private String result = "OK";
|
||||
private String errorMessage;
|
||||
private String requestId;
|
||||
private String detail;
|
||||
private String processRunId;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor.
|
||||
*******************************************************************************/
|
||||
public Entry(StorageCaller caller, String action, String bucketAlias)
|
||||
{
|
||||
this.caller = caller;
|
||||
this.action = action;
|
||||
this.bucketAlias = bucketAlias;
|
||||
this.processRunId = caller.getRunId();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Set the key.
|
||||
*******************************************************************************/
|
||||
public Entry key(String key)
|
||||
{
|
||||
this.key = key;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Set the destination key.
|
||||
*******************************************************************************/
|
||||
public Entry destination(String destinationKey)
|
||||
{
|
||||
this.destinationKey = destinationKey;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Set the byte count.
|
||||
*******************************************************************************/
|
||||
public Entry bytes(Long bytes)
|
||||
{
|
||||
this.bytes = bytes;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Set the provider request id.
|
||||
*******************************************************************************/
|
||||
public Entry requestId(String requestId)
|
||||
{
|
||||
this.requestId = requestId;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Set the detail text.
|
||||
*******************************************************************************/
|
||||
public Entry detail(String detail)
|
||||
{
|
||||
this.detail = detail;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Set the process run id (UI actions).
|
||||
*******************************************************************************/
|
||||
public Entry processRunId(String processRunId)
|
||||
{
|
||||
this.processRunId = processRunId;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Mark failed.
|
||||
*******************************************************************************/
|
||||
public Entry failed(String errorMessage)
|
||||
{
|
||||
this.result = "FAILED";
|
||||
this.errorMessage = errorMessage;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Mark skipped.
|
||||
*******************************************************************************/
|
||||
public Entry skipped(String reason)
|
||||
{
|
||||
this.result = "SKIPPED";
|
||||
this.errorMessage = reason;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Write it.
|
||||
*******************************************************************************/
|
||||
public void write()
|
||||
{
|
||||
OperationLog.write(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Insert the row (as the system user), never throwing.
|
||||
*******************************************************************************/
|
||||
private static void write(Entry e)
|
||||
{
|
||||
if(!Db.available(ObjectStorageTables.OPERATION))
|
||||
{
|
||||
LOG.info("Storage operation (no operation table in context)", logPair("action", e.action), logPair("bucket", e.bucketAlias), logPair("key", e.key), logPair("result", e.result));
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
Integer bucketId = ConfigSync.bucketId(e.bucketAlias);
|
||||
Db.asSystem(() -> Db.insert(ObjectStorageTables.OPERATION, new QRecord()
|
||||
.withValue("occurredAt", ObjectStorageRuntime.clock().instant())
|
||||
.withValue("actor", e.caller.describe())
|
||||
.withValue("action", e.action)
|
||||
.withValue("bucketId", bucketId)
|
||||
.withValue("bucketAlias", e.bucketAlias)
|
||||
.withValue("objectKey", e.key)
|
||||
.withValue("destinationKey", e.destinationKey)
|
||||
.withValue("bytes", e.bytes)
|
||||
.withValue("result", e.result)
|
||||
.withValue("errorMessage", e.errorMessage)
|
||||
.withValue("providerRequestId", e.requestId)
|
||||
.withValue("processRunId", e.processRunId)
|
||||
.withValue("detail", e.detail)));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
LOG.warn("Could not write a storage operation log row", ex, logPair("action", e.action), logPair("key", e.key));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.internal;
|
||||
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Base64;
|
||||
import java.util.HexFormat;
|
||||
import com.kingsrook.qbits.objectstorage.api.PresignedPost;
|
||||
import com.kingsrook.qbits.objectstorage.api.StorageException;
|
||||
import com.kingsrook.qbits.objectstorage.config.AddressingStyle;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Signs a browser-form POST policy (SigV4) with a content-length-range
|
||||
** condition, which can express a maximum upload size. The AWS SDK v2 has no
|
||||
** helper for this.
|
||||
*******************************************************************************/
|
||||
public final class PostPolicySigner
|
||||
{
|
||||
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("yyyyMMdd").withZone(ZoneOffset.UTC);
|
||||
private static final DateTimeFormatter AMZ_DATE = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'").withZone(ZoneOffset.UTC);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private PostPolicySigner()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Build the URL and form fields.
|
||||
*******************************************************************************/
|
||||
public static PresignedPost sign(String accessKey, String secretKey, String region, String endpoint, AddressingStyle style, String bucket, String key,
|
||||
String contentType, Long minBytes, Long maxBytes, Instant now, Duration ttl)
|
||||
{
|
||||
if(accessKey == null || secretKey == null)
|
||||
{
|
||||
throw (new StorageException(StorageException.Reason.ACCESS_DENIED, "No static credentials are available to sign a POST policy"));
|
||||
}
|
||||
String date = DATE.format(now);
|
||||
String amzDate = AMZ_DATE.format(now);
|
||||
String credential = accessKey + "/" + date + "/" + region + "/s3/aws4_request";
|
||||
String expiration = DateTimeFormatter.ISO_INSTANT.format(now.plus(ttl));
|
||||
|
||||
StringBuilder policy = new StringBuilder("{\"expiration\":\"").append(expiration).append("\",\"conditions\":[");
|
||||
policy.append("{\"bucket\":\"").append(json(bucket)).append("\"},");
|
||||
policy.append("{\"key\":\"").append(json(key)).append("\"},");
|
||||
if(StringUtils.hasContent(contentType))
|
||||
{
|
||||
policy.append("{\"Content-Type\":\"").append(json(contentType)).append("\"},");
|
||||
}
|
||||
if(maxBytes != null)
|
||||
{
|
||||
policy.append("[\"content-length-range\",").append(minBytes == null ? 0 : minBytes).append(",").append(maxBytes).append("],");
|
||||
}
|
||||
policy.append("{\"x-amz-algorithm\":\"AWS4-HMAC-SHA256\"},{\"x-amz-credential\":\"").append(json(credential)).append("\"},{\"x-amz-date\":\"").append(amzDate).append("\"}]}");
|
||||
|
||||
String policyBase64 = Base64.getEncoder().encodeToString(policy.toString().getBytes(StandardCharsets.UTF_8));
|
||||
byte[] signingKey = hmac(hmac(hmac(hmac(("AWS4" + secretKey).getBytes(StandardCharsets.UTF_8), date), region), "s3"), "aws4_request");
|
||||
String signature = HexFormat.of().formatHex(hmac(signingKey, policyBase64));
|
||||
|
||||
PresignedPost post = new PresignedPost().withUrl(formUrl(endpoint, style, bucket, region));
|
||||
post.getFields().put("bucket", bucket);
|
||||
post.getFields().put("key", key);
|
||||
if(StringUtils.hasContent(contentType))
|
||||
{
|
||||
post.getFields().put("Content-Type", contentType);
|
||||
}
|
||||
post.getFields().put("x-amz-algorithm", "AWS4-HMAC-SHA256");
|
||||
post.getFields().put("x-amz-credential", credential);
|
||||
post.getFields().put("x-amz-date", amzDate);
|
||||
post.getFields().put("policy", policyBase64);
|
||||
post.getFields().put("x-amz-signature", signature);
|
||||
return (post);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The form action URL for the endpoint and addressing style.
|
||||
*******************************************************************************/
|
||||
static String formUrl(String endpoint, AddressingStyle style, String bucket, String region)
|
||||
{
|
||||
if(!StringUtils.hasContent(endpoint))
|
||||
{
|
||||
return ("https://" + bucket + ".s3." + region + ".amazonaws.com/");
|
||||
}
|
||||
URI uri = URI.create(endpoint);
|
||||
String base = uri.getScheme() + "://" + (style == AddressingStyle.VIRTUAL_HOST ? bucket + "." : "") + uri.getAuthority();
|
||||
return (style == AddressingStyle.VIRTUAL_HOST ? base + "/" : base + "/" + bucket);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** HMAC-SHA256.
|
||||
*******************************************************************************/
|
||||
private static byte[] hmac(byte[] key, String data)
|
||||
{
|
||||
try
|
||||
{
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(key, "HmacSHA256"));
|
||||
return (mac.doFinal(data.getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new IllegalStateException("HmacSHA256 is not available", e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Escape a JSON string value.
|
||||
*******************************************************************************/
|
||||
private static String json(String s)
|
||||
{
|
||||
return (s.replace("\\", "\\\\").replace("\"", "\\\""));
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.internal;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.objectstorage.api.StorageCaller;
|
||||
import com.kingsrook.qbits.objectstorage.model.ObjectStorageTables;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Evaluates object_storage_protection_rule: a protected bucket and prefix, with
|
||||
** a mode (READ_ONLY blocks every write, NO_DELETE blocks delete and move, WARN
|
||||
** only notes it) and optional allowedSystemCallers (for example "backups")
|
||||
** whose system-context permanent delete may pass a NO_DELETE rule.
|
||||
*******************************************************************************/
|
||||
public final class ProtectionRules
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(ProtectionRules.class);
|
||||
|
||||
/*******************************************************************************
|
||||
** What an operation is, for rule matching.
|
||||
*******************************************************************************/
|
||||
public enum Op
|
||||
{
|
||||
WRITE,
|
||||
DELETE
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The result of a check.
|
||||
*******************************************************************************/
|
||||
public record Result(boolean blocked, String reason, boolean warned)
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Not protected.
|
||||
*******************************************************************************/
|
||||
public static Result open()
|
||||
{
|
||||
return (new Result(false, null, false));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private ProtectionRules()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Check a key for an operation. Never throws: when the table cannot be read,
|
||||
** nothing is protected by rule (the bucket-level protectedBucket flag still
|
||||
** applies in the caller).
|
||||
*******************************************************************************/
|
||||
public static Result check(String bucketAlias, String key, Op op, StorageCaller caller)
|
||||
{
|
||||
if(!Db.available(ObjectStorageTables.PROTECTION_RULE))
|
||||
{
|
||||
return (Result.open());
|
||||
}
|
||||
try
|
||||
{
|
||||
Integer bucketId = ConfigSync.bucketId(bucketAlias);
|
||||
List<QRecord> rules = Db.asSystem(() -> Db.query(ObjectStorageTables.PROTECTION_RULE, "enabled", true));
|
||||
List<String> warnings = new ArrayList<>();
|
||||
for(QRecord rule : rules)
|
||||
{
|
||||
if(rule.getValueInteger("bucketId") != null && !rule.getValueInteger("bucketId").equals(bucketId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if(!Globs.matches(rule.getValueString("prefixGlob"), key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String mode = rule.getValueString("mode");
|
||||
String reason = "Protected by a rule" + (StringUtils.hasContent(rule.getValueString("reason")) ? ": " + rule.getValueString("reason") : "");
|
||||
if("WARN".equals(mode))
|
||||
{
|
||||
warnings.add(reason);
|
||||
}
|
||||
else if("READ_ONLY".equals(mode) || ("NO_DELETE".equals(mode) && op == Op.DELETE))
|
||||
{
|
||||
if(op == Op.DELETE && caller.isSystem() && allowed(rule.getValueString("allowedSystemCallers"), caller.getCallerName()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
return (new Result(true, reason, false));
|
||||
}
|
||||
}
|
||||
return (new Result(false, warnings.isEmpty() ? null : String.join("; ", warnings), !warnings.isEmpty()));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Could not evaluate protection rules", e);
|
||||
return (Result.open());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether a caller name is in a comma-separated allowed list.
|
||||
*******************************************************************************/
|
||||
private static boolean allowed(String allowedList, String callerName)
|
||||
{
|
||||
if(!StringUtils.hasContent(allowedList) || callerName == null)
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
for(String name : allowedList.split(","))
|
||||
{
|
||||
if(name.trim().equals(callerName))
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
}
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.internal;
|
||||
|
||||
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.objectstorage.api.Capability;
|
||||
import com.kingsrook.qbits.objectstorage.api.CapabilityStatus;
|
||||
import com.kingsrook.qbits.objectstorage.config.ProviderType;
|
||||
import static com.kingsrook.qbits.objectstorage.api.CapabilityStatus.PARTIAL;
|
||||
import static com.kingsrook.qbits.objectstorage.api.CapabilityStatus.SUPPORTED;
|
||||
import static com.kingsrook.qbits.objectstorage.api.CapabilityStatus.UNKNOWN;
|
||||
import static com.kingsrook.qbits.objectstorage.api.CapabilityStatus.UNSUPPORTED;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Seed capabilities per provider type (spec section 3.3). Cells the provider's
|
||||
** documentation does not state clearly are UNKNOWN, never a guess. The probe
|
||||
** overrides every one of these.
|
||||
*******************************************************************************/
|
||||
public final class ProviderDefaults
|
||||
{
|
||||
private static final Map<ProviderType, Map<Capability, CapabilityStatus>> TABLE = new EnumMap<>(ProviderType.class);
|
||||
|
||||
static
|
||||
{
|
||||
// versioning lock lifecycle policy cors acl tagging post conditional
|
||||
put(ProviderType.AWS_S3, SUPPORTED, SUPPORTED, SUPPORTED, SUPPORTED, SUPPORTED, PARTIAL, SUPPORTED, SUPPORTED, SUPPORTED);
|
||||
put(ProviderType.MINIO, SUPPORTED, UNKNOWN, SUPPORTED, PARTIAL, UNKNOWN, PARTIAL, SUPPORTED, SUPPORTED, SUPPORTED);
|
||||
put(ProviderType.CLOUDFLARE_R2, UNSUPPORTED, UNSUPPORTED, PARTIAL, UNSUPPORTED, SUPPORTED, UNSUPPORTED, UNSUPPORTED, UNKNOWN, UNKNOWN);
|
||||
put(ProviderType.BACKBLAZE_B2, UNKNOWN, UNKNOWN, UNKNOWN, UNKNOWN, UNKNOWN, PARTIAL, UNSUPPORTED, UNSUPPORTED, UNKNOWN);
|
||||
put(ProviderType.WASABI, SUPPORTED, SUPPORTED, UNKNOWN, SUPPORTED, SUPPORTED, PARTIAL, SUPPORTED, UNKNOWN, UNKNOWN);
|
||||
put(ProviderType.DO_SPACES, SUPPORTED, UNKNOWN, PARTIAL, PARTIAL, SUPPORTED, PARTIAL, UNKNOWN, UNKNOWN, UNKNOWN);
|
||||
put(ProviderType.CEPH_RGW, SUPPORTED, SUPPORTED, SUPPORTED, SUPPORTED, SUPPORTED, SUPPORTED, SUPPORTED, SUPPORTED, UNKNOWN);
|
||||
put(ProviderType.GARAGE, UNSUPPORTED, UNSUPPORTED, PARTIAL, UNSUPPORTED, SUPPORTED, UNSUPPORTED, UNSUPPORTED, SUPPORTED, UNKNOWN);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private ProviderDefaults()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** One provider's row.
|
||||
*******************************************************************************/
|
||||
private static void put(ProviderType type, CapabilityStatus versioning, CapabilityStatus lock, CapabilityStatus lifecycle, CapabilityStatus policy,
|
||||
CapabilityStatus cors, CapabilityStatus acl, CapabilityStatus tagging, CapabilityStatus post, CapabilityStatus conditional)
|
||||
{
|
||||
Map<Capability, CapabilityStatus> m = new EnumMap<>(Capability.class);
|
||||
m.put(Capability.VERSIONING, versioning);
|
||||
m.put(Capability.OBJECT_LOCK, lock);
|
||||
m.put(Capability.LIFECYCLE, lifecycle);
|
||||
m.put(Capability.BUCKET_POLICY, policy);
|
||||
m.put(Capability.CORS, cors);
|
||||
m.put(Capability.ACL, acl);
|
||||
m.put(Capability.OBJECT_TAGGING, tagging);
|
||||
m.put(Capability.PRESIGNED_POST, post);
|
||||
m.put(Capability.CONDITIONAL_WRITE, conditional);
|
||||
TABLE.put(type, m);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The default status of a capability for a provider type.
|
||||
*******************************************************************************/
|
||||
public static CapabilityStatus defaultStatus(ProviderType type, Capability capability)
|
||||
{
|
||||
switch(capability)
|
||||
{
|
||||
case LIST_V2, PUT_GET_DELETE, COPY_REPLACE_METADATA, BATCH_DELETE, MULTIPART, PRESIGNED_GET, PRESIGNED_PUT ->
|
||||
{
|
||||
return (type == ProviderType.OTHER ? UNKNOWN : SUPPORTED);
|
||||
}
|
||||
case USAGE_ADMIN_API, QUOTA_ADMIN_API ->
|
||||
{
|
||||
return (UNSUPPORTED);
|
||||
}
|
||||
default ->
|
||||
{
|
||||
Map<Capability, CapabilityStatus> m = TABLE.get(type);
|
||||
return (m == null ? UNKNOWN : m.getOrDefault(capability, UNKNOWN));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.internal;
|
||||
|
||||
|
||||
import java.util.concurrent.Semaphore;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A per-connection token bucket (operations per second) plus a concurrency cap.
|
||||
** One per app instance; multi-replica apps get per-replica limits.
|
||||
*******************************************************************************/
|
||||
public class RateLimiter
|
||||
{
|
||||
private final double opsPerSecond;
|
||||
private final Semaphore concurrency;
|
||||
|
||||
private double tokens;
|
||||
private long lastRefillNanos;
|
||||
private long throttledWaits = 0;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor.
|
||||
*******************************************************************************/
|
||||
public RateLimiter(int opsPerSecond, int maxConcurrency)
|
||||
{
|
||||
this.opsPerSecond = Math.max(1, opsPerSecond);
|
||||
this.concurrency = new Semaphore(Math.max(1, maxConcurrency));
|
||||
this.tokens = this.opsPerSecond;
|
||||
this.lastRefillNanos = System.nanoTime();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Wait for a token and a concurrency slot. Close the permit when the call ends.
|
||||
*******************************************************************************/
|
||||
public Permit acquire()
|
||||
{
|
||||
try
|
||||
{
|
||||
concurrency.acquire();
|
||||
waitForToken();
|
||||
return (new Permit(concurrency));
|
||||
}
|
||||
catch(InterruptedException e)
|
||||
{
|
||||
Thread.currentThread().interrupt();
|
||||
throw (new IllegalStateException("Interrupted while waiting for the storage rate limiter", e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Block until a token is available.
|
||||
*******************************************************************************/
|
||||
private void waitForToken() throws InterruptedException
|
||||
{
|
||||
while(true)
|
||||
{
|
||||
long sleepMillis;
|
||||
synchronized(this)
|
||||
{
|
||||
long now = System.nanoTime();
|
||||
tokens = Math.min(opsPerSecond, tokens + (now - lastRefillNanos) / 1_000_000_000.0 * opsPerSecond);
|
||||
lastRefillNanos = now;
|
||||
if(tokens >= 1.0)
|
||||
{
|
||||
tokens -= 1.0;
|
||||
return;
|
||||
}
|
||||
throttledWaits++;
|
||||
sleepMillis = Math.max(1, (long) ((1.0 - tokens) / opsPerSecond * 1000));
|
||||
}
|
||||
Thread.sleep(sleepMillis);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** How many times a caller had to wait for a token.
|
||||
*******************************************************************************/
|
||||
public synchronized long getThrottledWaits()
|
||||
{
|
||||
return (throttledWaits);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Releases the concurrency slot.
|
||||
*******************************************************************************/
|
||||
public static class Permit implements AutoCloseable
|
||||
{
|
||||
private final Semaphore semaphore;
|
||||
private boolean closed = false;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor.
|
||||
*******************************************************************************/
|
||||
Permit(Semaphore semaphore)
|
||||
{
|
||||
this.semaphore = semaphore;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Release.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void close()
|
||||
{
|
||||
if(!closed)
|
||||
{
|
||||
closed = true;
|
||||
semaphore.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.internal;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Resolves secret references (${env.X}, ${prop.X}, ${file.PATH}) and enforces
|
||||
** the allow-list. Resolved values live only in memory, never in a record, a log
|
||||
** line or a process value.
|
||||
**
|
||||
** STAND-IN(core=QRun-IO/qqq#1165, removeBy=4.1.0-RC.8)
|
||||
** RC.7 QMetaDataVariableInterpreter still supports env/prop, not file references.
|
||||
** ${file.PATH} moves to QMetaDataVariableInterpreter in QQQ core
|
||||
** (spec section 8.2); then this class delegates entirely.
|
||||
*******************************************************************************/
|
||||
public final class SecretRefs
|
||||
{
|
||||
public static final List<String> DEFAULT_ALLOWED_PATTERNS = List.of("env.S3_*", "env.STORAGE_*", "file./var/run/secrets/storage/*");
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private SecretRefs()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether the reference is of the form ${...} and its inside matches an
|
||||
** allowed glob.
|
||||
*******************************************************************************/
|
||||
public static boolean isAllowed(String ref, List<String> allowedPatterns)
|
||||
{
|
||||
if(!StringUtils.hasContent(ref))
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
String trimmed = ref.trim();
|
||||
if(!trimmed.startsWith("${") || !trimmed.endsWith("}"))
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
String inside = trimmed.substring(2, trimmed.length() - 1);
|
||||
for(String pattern : allowedPatterns)
|
||||
{
|
||||
if(globToRegex(pattern).matcher(inside).matches())
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
}
|
||||
return (false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The value a reference points at, or null when it is empty or unset.
|
||||
*******************************************************************************/
|
||||
public static String resolve(String ref)
|
||||
{
|
||||
if(!StringUtils.hasContent(ref))
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
String trimmed = ref.trim();
|
||||
if(trimmed.startsWith("${file.") && trimmed.endsWith("}"))
|
||||
{
|
||||
try
|
||||
{
|
||||
return (Files.readString(Path.of(trimmed.substring("${file.".length(), trimmed.length() - 1)), StandardCharsets.UTF_8).trim());
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
}
|
||||
String value = new QMetaDataVariableInterpreter().interpret(trimmed);
|
||||
return (value == null || value.startsWith("${") ? null : value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Resolve a non-secret setting (endpoint, region) that may be a variable.
|
||||
*******************************************************************************/
|
||||
public static String resolveSetting(String value)
|
||||
{
|
||||
return (StringUtils.hasContent(value) ? resolve(value) : null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Glob (only * is special) to regex.
|
||||
*******************************************************************************/
|
||||
private static Pattern globToRegex(String glob)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for(char c : glob.toCharArray())
|
||||
{
|
||||
sb.append(c == '*' ? ".*" : Pattern.quote(String.valueOf(c)));
|
||||
}
|
||||
return (Pattern.compile(sb.toString()));
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.model;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.objectstorage.api.ObjectStorage;
|
||||
import com.kingsrook.qbits.objectstorage.config.BucketRole;
|
||||
import com.kingsrook.qbits.objectstorage.config.StorageBucketConfig;
|
||||
import com.kingsrook.qbits.objectstorage.internal.ConfigSync;
|
||||
import com.kingsrook.qbits.objectstorage.internal.ObjectStorageRuntime;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostQueryCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fills the virtual previewUrl field of object records with a short-lived
|
||||
** (5 minute) presigned GET, so the FILE_PREVIEW adornment can show a thumbnail
|
||||
** and a lightbox without the browser ever holding a credential. Only records the
|
||||
** user could query get one (record locks applied first), and BACKUP buckets get
|
||||
** none. At most 200 records per query.
|
||||
*******************************************************************************/
|
||||
public class ObjectPreviewCustomizer extends AbstractPostQueryCustomizer
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(ObjectPreviewCustomizer.class);
|
||||
|
||||
private static final int MAX_PER_QUERY = 200;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Add the preview URLs.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> apply(List<QRecord> records)
|
||||
{
|
||||
if(!ObjectStorageRuntime.isInstalled())
|
||||
{
|
||||
return (records);
|
||||
}
|
||||
int done = 0;
|
||||
for(QRecord record : records)
|
||||
{
|
||||
if(done >= MAX_PER_QUERY)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if(Boolean.TRUE.equals(record.getValueBoolean("deleted")) || record.getValueString("objectKey") == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
try
|
||||
{
|
||||
String alias = ConfigSync.aliasOf(record.getValueInteger("bucketId"));
|
||||
if(alias == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
StorageBucketConfig bucket = ObjectStorageRuntime.getConfig().findBucket(alias);
|
||||
if(bucket == null || bucket.getRole() == BucketRole.BACKUP)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
record.setValue("previewUrl", ObjectStorage.bucket(alias).previewUrl(record.getValueString("objectKey")).toString());
|
||||
done++;
|
||||
}
|
||||
catch(RuntimeException e)
|
||||
{
|
||||
LOG.debug("Could not make a preview URL", e);
|
||||
}
|
||||
}
|
||||
return (records);
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.model;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
import com.kingsrook.qbits.objectstorage.ObjectStorageQBitConfig;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Applies help/qbit-object-storage.yaml (the QQQ help-bundle format) to the
|
||||
** Storage app and its object_storage_* tables: what each table is for and how
|
||||
** its rows get created, shown on the table screen and when it is empty.
|
||||
** Inline defaults preserve empty-state help for hosts without helpContent.
|
||||
** The producer also registers the bundle for core sync and HOST overrides.
|
||||
*******************************************************************************/
|
||||
public final class ObjectStorageHelp
|
||||
{
|
||||
public static final String RESOURCE = "/help/qbit-object-storage.yaml";
|
||||
public static final String SLOT = "help";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private ObjectStorageHelp()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The bundle's entries by key, validated (fails startup if malformed).
|
||||
*******************************************************************************/
|
||||
public static Map<String, JsonNode> entries() throws QException
|
||||
{
|
||||
try(InputStream stream = ObjectStorageHelp.class.getResourceAsStream(RESOURCE))
|
||||
{
|
||||
if(stream == null)
|
||||
{
|
||||
throw (new IOException("missing resource"));
|
||||
}
|
||||
JsonNode bundle = new ObjectMapper(new YAMLFactory()).readTree(stream);
|
||||
if(!"qbit-object-storage".equals(bundle.path("bundle").asText()) || !"QBIT".equals(bundle.path("layer").asText()))
|
||||
{
|
||||
throw (new IOException("invalid bundle header"));
|
||||
}
|
||||
Map<String, JsonNode> entries = new LinkedHashMap<>();
|
||||
for(JsonNode entry : bundle.path("entries"))
|
||||
{
|
||||
String key = entry.path("key").asText();
|
||||
String summary = entry.path("summary").asText();
|
||||
if(key.isBlank() || summary.isBlank() || summary.length() > 120 || entries.put(key, entry) != null)
|
||||
{
|
||||
throw (new IOException("invalid or duplicate entry: " + key));
|
||||
}
|
||||
}
|
||||
return (entries);
|
||||
}
|
||||
catch(IOException | RuntimeException e)
|
||||
{
|
||||
throw (new QException("Could not load " + RESOURCE, e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Apply the help to the app and every object_storage_* table in the
|
||||
** instance. A host's own help on a table or the app is kept (the bundle only
|
||||
** fills the slot).
|
||||
*******************************************************************************/
|
||||
public static void apply(QInstance qInstance, ObjectStorageQBitConfig config) throws QException
|
||||
{
|
||||
Map<String, JsonNode> entries = entries();
|
||||
for(QTableMetaData table : qInstance.getTables().values())
|
||||
{
|
||||
JsonNode entry = entries.get("table:" + table.getName() + ";slot:" + SLOT);
|
||||
if(entry != null && (table.getHelpContent() == null || !table.getHelpContent().containsKey(SLOT)))
|
||||
{
|
||||
table.withHelpContent(SLOT, content(entry));
|
||||
}
|
||||
}
|
||||
QAppMetaData app = qInstance.getApp(config.getAppName());
|
||||
JsonNode appEntry = entries.get("app:objectStorageAdmin;slot:" + SLOT);
|
||||
if(app != null && appEntry != null && (app.getHelpContent() == null || !app.getHelpContent().containsKey(SLOT)))
|
||||
{
|
||||
app.withHelpContent(SLOT, content(appEntry));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Summary as the first paragraph, then the content, as markdown.
|
||||
*******************************************************************************/
|
||||
static QHelpContent content(JsonNode entry)
|
||||
{
|
||||
return (new QHelpContent().withContentAsMarkdown(entry.path("summary").asText().strip() + "\n\n" + entry.path("content").asText().strip()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,283 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.model;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.objectstorage.ObjectStorageQBitConfig;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The PostgreSQL schema of the qbit's tables, generated from the same metadata
|
||||
** the app runs on, so an adopter's migration (Liquibase, Flyway) cannot drift
|
||||
** from the tables. Column names are the snake_case of the field names (what the
|
||||
** QQQ RDBMS backend uses), table names are the QQQ table names.
|
||||
**
|
||||
** The statements are idempotent (CREATE ... IF NOT EXISTS) and include the
|
||||
** unique constraints and the browse indexes the design calls for
|
||||
** ((bucket, key) unique; bucket with folder, last modified, size, content type),
|
||||
** then the foreign keys, each in a block that also upgrades an existing schema
|
||||
** (orphans recorded in qqq_fk_quarantine first; see addForeignKey).
|
||||
*******************************************************************************/
|
||||
public final class ObjectStorageSchema
|
||||
{
|
||||
/** Unique constraints: table to column lists. */
|
||||
private static final Map<String, List<List<String>>> UNIQUES = Map.of(
|
||||
ObjectStorageTables.CONNECTION, List.of(List.of("name")),
|
||||
ObjectStorageTables.BUCKET, List.of(List.of("alias")),
|
||||
ObjectStorageTables.CONNECTION_CAPABILITY, List.of(List.of("connectionId", "capability")),
|
||||
ObjectStorageTables.BUCKET_CAPABILITY, List.of(List.of("bucketId", "capability")),
|
||||
ObjectStorageTables.FOLDER, List.of(List.of("bucketId", "path")),
|
||||
ObjectStorageTables.OBJECT, List.of(List.of("bucketId", "objectKey")),
|
||||
ObjectStorageTables.USAGE_DAILY, List.of(List.of("bucketId", "snapshotDate")));
|
||||
|
||||
/** Browse and lookup indexes: table to column lists. New entries go at the end of
|
||||
** a table's list, so the generated names (ix_<table>_<n>) of existing ones never change.
|
||||
** The child lists on record pages (folder -> objects, object -> versions and references)
|
||||
** filter on the parent id alone, so those columns lead an index of their own. */
|
||||
private static final Map<String, List<List<String>>> INDEXES = Map.of(
|
||||
ObjectStorageTables.OBJECT, List.of(List.of("bucketId", "folderId"), List.of("bucketId", "lastModified"), List.of("bucketId", "size"), List.of("bucketId", "contentType"),
|
||||
List.of("bucketId", "deleted", "referenceCount"), List.of("bucketId", "headersKnown", "deleted"), List.of("folderId")),
|
||||
ObjectStorageTables.OBJECT_REFERENCE, List.of(List.of("bucketId", "objectId"), List.of("bucketId", "missing"), List.of("objectId")),
|
||||
ObjectStorageTables.OPERATION, List.of(List.of("bucketId", "occurredAt"), List.of("objectKey", "occurredAt")),
|
||||
ObjectStorageTables.FOLDER, List.of(List.of("parentFolderId")),
|
||||
ObjectStorageTables.OBJECT_VERSION, List.of(List.of("bucketId", "objectKey"), List.of("objectId")),
|
||||
ObjectStorageTables.BUCKET, List.of(List.of("connectionId")),
|
||||
ObjectStorageTables.HEADER_RULE, List.of(List.of("bucketId")),
|
||||
ObjectStorageTables.PROTECTION_RULE, List.of(List.of("bucketId")));
|
||||
|
||||
/** The table that records the rows an upgrade changed so a foreign key could be added
|
||||
** (identical DDL in every qbit, so whichever runs first creates it). */
|
||||
public static final String QUARANTINE_DDL = "CREATE TABLE IF NOT EXISTS qqq_fk_quarantine (id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, "
|
||||
+ "table_name VARCHAR(100) NOT NULL, column_name VARCHAR(100) NOT NULL, row_id VARCHAR(100), missing_parent_id VARCHAR(100), action VARCHAR(20) NOT NULL, "
|
||||
+ "row_data TEXT, changeset VARCHAR(200), quarantined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)";
|
||||
|
||||
/** Every parent/child relation, enforced in the database. The qbit never deletes a
|
||||
** connection, bucket or folder row (ConfigSync only upserts; folders are only
|
||||
** created), and its only deletes are object rows (scan removal, trash retention,
|
||||
** hard-delete write-through) and reference rows (rebuilt by each reference scan),
|
||||
** through QQQ deletes with no associations. So:
|
||||
** - observed state that belongs to a parent (capabilities, folders, objects, versions,
|
||||
** usage) CASCADEs: an object delete takes its versions, and an admin deleting a
|
||||
** bucket row takes its index with it;
|
||||
** - optional pointers into the index (object.folderId, reference.objectId) SET NULL:
|
||||
** the next scan sets the folder again, and a reference to a removed object is
|
||||
** missing until the next reference scan rebuilds the rows;
|
||||
** - admin-authored config (header and protection rules), the operation audit log and
|
||||
** a bucket's connection RESTRICT (NO ACTION): deleting a bucket row that has rules
|
||||
** or history, or a connection that buckets use, is refused rather than silently
|
||||
** dropping protection or audit rows (ConfigSync would re-create a configured bucket
|
||||
** under a new id, with its rules gone).
|
||||
** Inserts are parents first everywhere (connections before buckets, a folder's parent
|
||||
** before the folder, folders before objects), so nothing needs to be deferred.
|
||||
** New entries go at the end. */
|
||||
private static final List<ForeignKey> FOREIGN_KEYS = List.of(
|
||||
new ForeignKey(ObjectStorageTables.BUCKET, "connectionId", ObjectStorageTables.CONNECTION, "NO ACTION"),
|
||||
new ForeignKey(ObjectStorageTables.CONNECTION_CAPABILITY, "connectionId", ObjectStorageTables.CONNECTION, "CASCADE"),
|
||||
new ForeignKey(ObjectStorageTables.BUCKET_CAPABILITY, "bucketId", ObjectStorageTables.BUCKET, "CASCADE"),
|
||||
new ForeignKey(ObjectStorageTables.HEADER_RULE, "bucketId", ObjectStorageTables.BUCKET, "NO ACTION"),
|
||||
new ForeignKey(ObjectStorageTables.PROTECTION_RULE, "bucketId", ObjectStorageTables.BUCKET, "NO ACTION"),
|
||||
new ForeignKey(ObjectStorageTables.OPERATION, "bucketId", ObjectStorageTables.BUCKET, "NO ACTION"),
|
||||
new ForeignKey(ObjectStorageTables.USAGE_DAILY, "bucketId", ObjectStorageTables.BUCKET, "CASCADE"),
|
||||
new ForeignKey(ObjectStorageTables.FOLDER, "bucketId", ObjectStorageTables.BUCKET, "CASCADE"),
|
||||
new ForeignKey(ObjectStorageTables.FOLDER, "parentFolderId", ObjectStorageTables.FOLDER, "CASCADE"),
|
||||
new ForeignKey(ObjectStorageTables.OBJECT, "bucketId", ObjectStorageTables.BUCKET, "CASCADE"),
|
||||
new ForeignKey(ObjectStorageTables.OBJECT, "folderId", ObjectStorageTables.FOLDER, "SET NULL"),
|
||||
new ForeignKey(ObjectStorageTables.OBJECT_VERSION, "bucketId", ObjectStorageTables.BUCKET, "CASCADE"),
|
||||
new ForeignKey(ObjectStorageTables.OBJECT_VERSION, "objectId", ObjectStorageTables.OBJECT, "CASCADE"),
|
||||
new ForeignKey(ObjectStorageTables.OBJECT_REFERENCE, "bucketId", ObjectStorageTables.BUCKET, "CASCADE"),
|
||||
new ForeignKey(ObjectStorageTables.OBJECT_REFERENCE, "objectId", ObjectStorageTables.OBJECT, "SET NULL"));
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** One foreign key: the child table and field, the parent table (always its id)
|
||||
** and the ON DELETE rule.
|
||||
*******************************************************************************/
|
||||
public record ForeignKey(String table, String field, String parent, String onDelete)
|
||||
{
|
||||
/*******************************************************************************
|
||||
** The constraint name: fk_<child table>_<child column>.
|
||||
*******************************************************************************/
|
||||
public String name()
|
||||
{
|
||||
return ("fk_" + table + "_" + column(field));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private ObjectStorageSchema()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The CREATE TABLE and CREATE INDEX statements for every qbit table, in
|
||||
** dependency order, one statement per list entry.
|
||||
*******************************************************************************/
|
||||
public static List<String> postgresStatements(ObjectStorageQBitConfig config)
|
||||
{
|
||||
List<String> statements = new ArrayList<>();
|
||||
List<String> order = List.of(ObjectStorageTables.CONNECTION, ObjectStorageTables.CONNECTION_CAPABILITY, ObjectStorageTables.BUCKET, ObjectStorageTables.BUCKET_CAPABILITY,
|
||||
ObjectStorageTables.FOLDER, ObjectStorageTables.OBJECT, ObjectStorageTables.OBJECT_VERSION, ObjectStorageTables.OBJECT_REFERENCE, ObjectStorageTables.HEADER_RULE,
|
||||
ObjectStorageTables.PROTECTION_RULE, ObjectStorageTables.OPERATION, ObjectStorageTables.USAGE_DAILY);
|
||||
List<QTableMetaData> tables = ObjectStorageTables.tables(config);
|
||||
for(String name : order)
|
||||
{
|
||||
for(QTableMetaData table : tables)
|
||||
{
|
||||
if(table.getName().equals(name))
|
||||
{
|
||||
statements.add(createTable(table));
|
||||
int n = 0;
|
||||
for(List<String> columns : INDEXES.getOrDefault(name, List.of()))
|
||||
{
|
||||
n++;
|
||||
statements.add("CREATE INDEX IF NOT EXISTS ix_" + name + "_" + n + " ON " + name + " (" + String.join(", ", columns.stream().map(ObjectStorageSchema::column).toList()) + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
statements.add(QUARANTINE_DDL);
|
||||
for(ForeignKey fk : FOREIGN_KEYS)
|
||||
{
|
||||
statements.add(addForeignKey(fk, tables));
|
||||
}
|
||||
return (statements);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The foreign keys the schema declares.
|
||||
*******************************************************************************/
|
||||
public static List<ForeignKey> foreignKeys()
|
||||
{
|
||||
return (FOREIGN_KEYS);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** One idempotent block that adds a foreign key and upgrades an existing schema
|
||||
** safely: rows whose parent is gone are first recorded in qqq_fk_quarantine, then
|
||||
** their reference is cleared (nullable column) or the row is deleted (required
|
||||
** column; the whole row is recorded), so adding the constraint cannot fail. A
|
||||
** no-op once the constraint exists.
|
||||
*******************************************************************************/
|
||||
private static String addForeignKey(ForeignKey fk, List<QTableMetaData> tables)
|
||||
{
|
||||
String col = column(fk.field());
|
||||
boolean required = tables.stream().filter(t -> t.getName().equals(fk.table())).findFirst()
|
||||
.map(t -> Boolean.TRUE.equals(t.getField(fk.field()).getIsRequired())).orElse(false);
|
||||
String orphans = "c." + col + " IS NOT NULL AND NOT EXISTS (SELECT 1 FROM " + fk.parent() + " p WHERE p.id = c." + col + ")";
|
||||
return ("DO $$\n"
|
||||
+ "BEGIN\n"
|
||||
+ " IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = '" + fk.name() + "' AND conrelid = '" + fk.table() + "'::regclass) THEN\n"
|
||||
+ " INSERT INTO qqq_fk_quarantine (table_name, column_name, row_id, missing_parent_id, action, row_data, changeset)\n"
|
||||
+ " SELECT '" + fk.table() + "', '" + col + "', c.id::text, c." + col + "::text, '" + (required ? "DELETED" : "NULLED") + "', row_to_json(c)::text, "
|
||||
+ "'qbit-object-storage ObjectStorageSchema'\n"
|
||||
+ " FROM " + fk.table() + " c WHERE " + orphans + ";\n"
|
||||
+ (required
|
||||
? " DELETE FROM " + fk.table() + " c WHERE " + orphans + ";\n"
|
||||
: " UPDATE " + fk.table() + " c SET " + col + " = NULL WHERE " + orphans + ";\n")
|
||||
+ " ALTER TABLE " + fk.table() + " ADD CONSTRAINT " + fk.name() + " FOREIGN KEY (" + col + ") REFERENCES " + fk.parent() + " (id) ON DELETE " + fk.onDelete() + ";\n"
|
||||
+ " END IF;\n"
|
||||
+ "END $$");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The statements as one script.
|
||||
*******************************************************************************/
|
||||
public static String postgresScript(ObjectStorageQBitConfig config)
|
||||
{
|
||||
return (String.join(";\n", postgresStatements(config)) + ";\n");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** One table.
|
||||
*******************************************************************************/
|
||||
private static String createTable(QTableMetaData table)
|
||||
{
|
||||
List<String> lines = new ArrayList<>();
|
||||
for(QFieldMetaData field : table.getFields().values())
|
||||
{
|
||||
if("id".equals(field.getName()))
|
||||
{
|
||||
lines.add(" id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL");
|
||||
continue;
|
||||
}
|
||||
lines.add(" " + column(field.getName()) + " " + sqlType(field) + (Boolean.TRUE.equals(field.getIsRequired()) ? " NOT NULL" : ""));
|
||||
}
|
||||
lines.add(" CONSTRAINT pk_" + table.getName() + " PRIMARY KEY (id)");
|
||||
int n = 0;
|
||||
for(List<String> columns : UNIQUES.getOrDefault(table.getName(), List.of()))
|
||||
{
|
||||
n++;
|
||||
lines.add(" CONSTRAINT uq_" + table.getName() + "_" + n + " UNIQUE (" + String.join(", ", columns.stream().map(ObjectStorageSchema::column).toList()) + ")");
|
||||
}
|
||||
return ("CREATE TABLE IF NOT EXISTS " + table.getName() + " (\n" + String.join(",\n", lines) + "\n)");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The snake_case column of a field name.
|
||||
*******************************************************************************/
|
||||
static String column(String fieldName)
|
||||
{
|
||||
return (QInstanceEnricher.inferBackendName(fieldName));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The PostgreSQL type of a field.
|
||||
*******************************************************************************/
|
||||
private static String sqlType(QFieldMetaData field)
|
||||
{
|
||||
return (switch(field.getType())
|
||||
{
|
||||
case INTEGER -> "INTEGER";
|
||||
case LONG -> "BIGINT";
|
||||
case BOOLEAN -> "BOOLEAN";
|
||||
case DATE -> "DATE";
|
||||
case DATE_TIME -> "TIMESTAMP WITHOUT TIME ZONE";
|
||||
case TEXT, HTML -> "TEXT";
|
||||
case DECIMAL -> "NUMERIC(18,4)";
|
||||
default -> "VARCHAR(" + (field.getMaxLength() == null ? 250 : field.getMaxLength()) + ")";
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,763 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.model;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.objectstorage.ObjectStorageQBitConfig;
|
||||
import com.kingsrook.qbits.objectstorage.permissions.ObjectStoragePermissionChecker;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.ChildRecordListRenderer;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.SectionFactory;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The qbit's tables: standard, visible QQQ tables named object_storage_*
|
||||
** (lowercase snake_case, the full qbit name as prefix). Field names are
|
||||
** camelCase. No table stores a secret value.
|
||||
*******************************************************************************/
|
||||
public final class ObjectStorageTables
|
||||
{
|
||||
public static final String CONNECTION = "object_storage_connection";
|
||||
public static final String CONNECTION_CAPABILITY = "object_storage_connection_capability";
|
||||
public static final String BUCKET = "object_storage_bucket";
|
||||
public static final String BUCKET_CAPABILITY = "object_storage_bucket_capability";
|
||||
public static final String FOLDER = "object_storage_folder";
|
||||
public static final String OBJECT = "object_storage_object";
|
||||
public static final String OBJECT_VERSION = "object_storage_object_version";
|
||||
public static final String OBJECT_REFERENCE = "object_storage_object_reference";
|
||||
public static final String HEADER_RULE = "object_storage_header_rule";
|
||||
public static final String PROTECTION_RULE = "object_storage_protection_rule";
|
||||
public static final String OPERATION = "object_storage_operation";
|
||||
public static final String USAGE_DAILY = "object_storage_usage_daily";
|
||||
|
||||
/** Tables written only by the qbit (history and observed state): read-only in the UI and API. */
|
||||
public static final List<String> READ_ONLY = List.of(CONNECTION_CAPABILITY, BUCKET_CAPABILITY, FOLDER, OBJECT, OBJECT_VERSION, OBJECT_REFERENCE, OPERATION, USAGE_DAILY);
|
||||
|
||||
public static final List<String> ALL = List.of(CONNECTION, CONNECTION_CAPABILITY, BUCKET, BUCKET_CAPABILITY, FOLDER, OBJECT, OBJECT_VERSION,
|
||||
OBJECT_REFERENCE, HEADER_RULE, PROTECTION_RULE, OPERATION, USAGE_DAILY);
|
||||
|
||||
public static final String KEY_TYPE_BUCKET = "object_storage_bucket";
|
||||
public static final String KEY_TYPE_CONNECTION = "object_storage_connection";
|
||||
public static final String ALL_ACCESS_BUCKET = "object_storage_bucket_all_access";
|
||||
public static final String ALL_ACCESS_CONN = "object_storage_connection_all_access";
|
||||
|
||||
public static final String PVS_PROVIDER_TYPE = "object_storage_provider_type";
|
||||
public static final String PVS_BUCKET_ROLE = "object_storage_bucket_role";
|
||||
public static final String PVS_MANAGED_BY = "object_storage_managed_by";
|
||||
public static final String PVS_CAPABILITY = "object_storage_capability";
|
||||
public static final String PVS_CAP_STATUS = "object_storage_capability_status";
|
||||
public static final String PVS_CAP_SOURCE = "object_storage_capability_source";
|
||||
public static final String PVS_PROTECTION = "object_storage_protection_mode";
|
||||
public static final String PVS_OPERATION = "object_storage_operation_action";
|
||||
public static final String PVS_RESULT = "object_storage_operation_result";
|
||||
public static final String PVS_SOURCE = "object_storage_config_source";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private ObjectStorageTables()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Permission rules shared by every qbit table, app, widget and process.
|
||||
*******************************************************************************/
|
||||
public static QPermissionRules permissionRules()
|
||||
{
|
||||
return (new QPermissionRules()
|
||||
.withLevel(PermissionLevel.HAS_ACCESS_PERMISSION)
|
||||
.withPermissionBaseName(ObjectStorageQBitConfig.PERMISSION_BASE_NAME)
|
||||
.withCustomPermissionChecker(new QCodeReference(ObjectStoragePermissionChecker.class)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The enum possible-value sources and the table references.
|
||||
*******************************************************************************/
|
||||
public static List<QPossibleValueSource> possibleValueSources()
|
||||
{
|
||||
List<QPossibleValueSource> list = new ArrayList<>();
|
||||
list.add(enumSource(PVS_PROVIDER_TYPE, "AWS_S3", "MINIO", "CLOUDFLARE_R2", "BACKBLAZE_B2", "WASABI", "DO_SPACES", "CEPH_RGW", "GARAGE", "OTHER"));
|
||||
list.add(enumSource(PVS_BUCKET_ROLE, "PUBLIC", "PRIVATE", "INCOMING", "BACKUP"));
|
||||
list.add(enumSource(PVS_MANAGED_BY, "IAC", "UI"));
|
||||
list.add(enumSource(PVS_CAPABILITY, "LIST_V2", "PUT_GET_DELETE", "COPY_REPLACE_METADATA", "BATCH_DELETE", "MULTIPART", "PRESIGNED_GET", "PRESIGNED_PUT",
|
||||
"PRESIGNED_POST", "CONDITIONAL_WRITE", "VERSIONING", "OBJECT_LOCK", "LIFECYCLE", "BUCKET_POLICY", "CORS", "OBJECT_TAGGING", "ACL", "USAGE_ADMIN_API", "QUOTA_ADMIN_API"));
|
||||
list.add(enumSource(PVS_CAP_STATUS, "SUPPORTED", "UNSUPPORTED", "NOT_PERMITTED", "PARTIAL", "UNKNOWN"));
|
||||
list.add(enumSource(PVS_CAP_SOURCE, "DEFAULT", "PROBE", "OVERRIDE"));
|
||||
list.add(enumSource(PVS_PROTECTION, "READ_ONLY", "NO_DELETE", "WARN"));
|
||||
list.add(enumSource(PVS_OPERATION, "PUT", "COPY", "MOVE", "HEADERS", "DELETE", "RESTORE", "PRESIGN_GET", "PRESIGN_PUT", "PRESIGN_POST", "UPLOAD", "SCAN", "PROBE", "PURGE_TRASH"));
|
||||
list.add(enumSource(PVS_RESULT, "OK", "FAILED", "SKIPPED"));
|
||||
list.add(enumSource(PVS_SOURCE, "CONFIG", "UI"));
|
||||
for(String table : ALL)
|
||||
{
|
||||
list.add(QPossibleValueSource.newForTable(table));
|
||||
}
|
||||
return (list);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** An enum possible-value source from plain string ids (the id is the label).
|
||||
*******************************************************************************/
|
||||
private static QPossibleValueSource enumSource(String name, String... values)
|
||||
{
|
||||
List<QPossibleValue<?>> list = new ArrayList<>();
|
||||
for(String value : values)
|
||||
{
|
||||
list.add(new QPossibleValue<>(value, labelOf(value)));
|
||||
}
|
||||
return (new QPossibleValueSource().withName(name).withType(QPossibleValueSourceType.ENUM).withEnumValues(list));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A readable label for an UPPER_SNAKE id.
|
||||
*******************************************************************************/
|
||||
static String labelOf(String id)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for(String part : id.toLowerCase().split("_"))
|
||||
{
|
||||
if(!part.isEmpty())
|
||||
{
|
||||
sb.append(sb.isEmpty() ? "" : " ").append(Character.toUpperCase(part.charAt(0))).append(part.substring(1));
|
||||
}
|
||||
}
|
||||
return (sb.toString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The security key types for per-bucket and per-connection scoping.
|
||||
*******************************************************************************/
|
||||
public static List<QSecurityKeyType> securityKeyTypes()
|
||||
{
|
||||
return (List.of(
|
||||
new QSecurityKeyType().withName(KEY_TYPE_BUCKET).withAllAccessKeyName(ALL_ACCESS_BUCKET).withPossibleValueSourceName(BUCKET),
|
||||
new QSecurityKeyType().withName(KEY_TYPE_CONNECTION).withAllAccessKeyName(ALL_ACCESS_CONN).withPossibleValueSourceName(CONNECTION)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Parent to child relations: {parent, child, foreign key on the child, label of the child list}. */
|
||||
private static final List<String[]> RELATIONS = List.of(
|
||||
new String[] { CONNECTION, BUCKET, "connectionId", "Buckets" },
|
||||
new String[] { CONNECTION, CONNECTION_CAPABILITY, "connectionId", "Capabilities" },
|
||||
new String[] { BUCKET, BUCKET_CAPABILITY, "bucketId", "Capabilities" },
|
||||
new String[] { BUCKET, FOLDER, "bucketId", "Folders" },
|
||||
new String[] { BUCKET, OBJECT, "bucketId", "Objects" },
|
||||
new String[] { BUCKET, OBJECT_REFERENCE, "bucketId", "References" },
|
||||
new String[] { BUCKET, USAGE_DAILY, "bucketId", "Usage" },
|
||||
new String[] { BUCKET, OPERATION, "bucketId", "Operations" },
|
||||
new String[] { BUCKET, HEADER_RULE, "bucketId", "Header rules" },
|
||||
new String[] { BUCKET, PROTECTION_RULE, "bucketId", "Protection rules" },
|
||||
new String[] { FOLDER, OBJECT, "folderId", "Objects" },
|
||||
new String[] { OBJECT, OBJECT_VERSION, "objectId", "Versions" },
|
||||
new String[] { OBJECT, OBJECT_REFERENCE, "objectId", "Referenced by" });
|
||||
|
||||
/** The columns a child list shows for each child table (the rest is one click away on the record). */
|
||||
private static final Map<String, List<String>> CHILD_LIST_COLUMNS = Map.ofEntries(
|
||||
Map.entry(OBJECT, List.of("objectKey", "size", "contentType", "lastModified", "referenceCount")),
|
||||
Map.entry(FOLDER, List.of("path", "objectCount", "totalBytes")),
|
||||
Map.entry(OBJECT_REFERENCE, List.of("label", "keyOrPrefix", "missing", "sourceTable")),
|
||||
Map.entry(OPERATION, List.of("occurredAt", "action", "objectKey", "result", "actor")),
|
||||
Map.entry(USAGE_DAILY, List.of("snapshotDate", "objectCount", "totalBytes")),
|
||||
Map.entry(CONNECTION_CAPABILITY, List.of("capability", "status", "detail")),
|
||||
Map.entry(BUCKET_CAPABILITY, List.of("capability", "status", "detail")),
|
||||
Map.entry(BUCKET, List.of("alias", "role", "indexed", "objectCount")),
|
||||
Map.entry(HEADER_RULE, List.of("name", "keyGlob", "cacheControl", "priority", "enabled")),
|
||||
Map.entry(PROTECTION_RULE, List.of("prefixGlob", "mode", "enabled", "reason")),
|
||||
Map.entry(OBJECT_VERSION, List.of("versionId", "isDeleteMarker", "isLatest", "lastModified")));
|
||||
|
||||
/** Tables that are nav entries (the rest appear as relations and child lists on their parents). */
|
||||
public static final List<String> NAV_TABLES = List.of(BUCKET, OBJECT, FOLDER, CONNECTION, OPERATION, HEADER_RULE, PROTECTION_RULE);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** All tables, with their child lists exposed (relations on the parent record).
|
||||
*******************************************************************************/
|
||||
public static List<QTableMetaData> tables(ObjectStorageQBitConfig config)
|
||||
{
|
||||
List<QTableMetaData> tables = new ArrayList<>(List.of(connection(config), connectionCapability(config), bucket(config), bucketCapability(config),
|
||||
folder(config), object(config), objectVersion(config), objectReference(config), headerRule(config), protectionRule(config), operation(config), usageDaily(config)));
|
||||
for(QTableMetaData table : tables)
|
||||
{
|
||||
layout(table);
|
||||
QInstanceEnricher.setInferredFieldBackendNames(table);
|
||||
}
|
||||
for(String[] relation : RELATIONS)
|
||||
{
|
||||
for(QTableMetaData table : tables)
|
||||
{
|
||||
if(table.getName().equals(relation[0]))
|
||||
{
|
||||
table.withSection(new QFieldSection(joinName(relation), new QIcon("list"), Tier.T2).withLabel(relation[3]).withWidgetName(joinName(relation)));
|
||||
table.withExposedJoin(new ExposedJoin().withLabel(relation[3]).withJoinTable(relation[1]).withJoinPath(List.of(joinName(relation))));
|
||||
}
|
||||
}
|
||||
}
|
||||
return (tables);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Record screen sections per table: "T1" identity, "T2:Label" detail groups, "T3" bookkeeping. The first name of each row is the section. */
|
||||
private static final Map<String, String[][]> LAYOUTS = Map.ofEntries(
|
||||
Map.entry(CONNECTION, new String[][] {
|
||||
{ "T1", "name", "providerType", "source", "lastTestedAt", "lastTestStatus" },
|
||||
{ "T2:Endpoint and signing", "endpoint", "presignEndpoint", "region", "addressingStyle", "checksumMode", "payloadSigning", "signatureVersion", "tlsVerify", "credentialsMode", "adminApiType" },
|
||||
{ "T2:Credentials (references only, never values)", "accessKeyRef", "secretKeyRef", "presignAccessKeyRef", "presignSecretKeyRef", "credentialsResolved", "presignKeyConfigured", "presignWarning" },
|
||||
{ "T2:Limits (editable with object_storage.admin)", "maxOpsPerSecond", "maxConcurrency", "connectTimeoutMs", "readTimeoutMs" },
|
||||
{ "T2:Last test", "lastTestSummary", "clockSkewSeconds" },
|
||||
{ "T3", "settingsHash", "id", "createDate", "modifyDate" } }),
|
||||
Map.entry(BUCKET, new String[][] {
|
||||
{ "T1", "alias", "role", "connectionId", "bucketName", "indexed", "managedBy", "referenceTracked", "source" },
|
||||
{ "T2:Observed settings", "versioningState", "lifecycleSummary", "policySummary", "corsSummary", "configDriftNote" },
|
||||
{ "T2:Index and health", "objectCount", "totalBytes", "lastScanDate", "lastScanStatus", "headersKnownCount", "headersMatchingPolicyCount", "orphanedCount", "orphanedBytes", "missingCount", "lastReferenceScanDate" },
|
||||
{ "T2:Settings", "publicBaseUrl", "trashBucketAlias", "trashPrefix", "maxPresignGetSeconds", "maxPresignPutSeconds", "orphanCountAlertThreshold", "quotaBytes" },
|
||||
{ "T3", "id", "scanGeneration", "scanCursor", "createDate", "modifyDate" } }),
|
||||
Map.entry(OBJECT, new String[][] {
|
||||
{ "T1", "previewUrl", "objectKey", "bucketId", "folderId", "size", "contentType", "lastModified", "referenceCount" },
|
||||
{ "T2:Headers and metadata", "cacheControl", "contentDisposition", "userMetadata", "tags", "matchesHeaderPolicy", "headersKnown" },
|
||||
{ "T2:Storage", "name", "extension", "etag", "storageClass", "versionId", "protection", "deleted" },
|
||||
{ "T3", "id", "scanGeneration", "indexedAt", "createDate", "modifyDate" } }),
|
||||
Map.entry(FOLDER, new String[][] {
|
||||
{ "T1", "path", "name", "bucketId", "parentFolderId", "objectCount", "totalBytes", "newestChange" },
|
||||
{ "T3", "id", "createDate", "modifyDate" } }),
|
||||
Map.entry(OBJECT_VERSION, new String[][] {
|
||||
{ "T1", "objectKey", "bucketId", "objectId", "versionId", "isDeleteMarker", "isLatest", "size", "lastModified" },
|
||||
{ "T3", "id", "createDate", "modifyDate" } }),
|
||||
Map.entry(OBJECT_REFERENCE, new String[][] {
|
||||
{ "T1", "label", "bucketId", "objectId", "keyOrPrefix", "matchMode", "missing" },
|
||||
{ "T2:Referencing record", "sourceTable", "sourceRecordId", "sourceField", "scannedAt" },
|
||||
{ "T3", "id", "createDate", "modifyDate" } }),
|
||||
Map.entry(HEADER_RULE, new String[][] {
|
||||
{ "T1", "name", "bucketId", "enabled", "priority" },
|
||||
{ "T2:Matches", "keyGlob", "keyRegex", "contentTypePattern" },
|
||||
{ "T2:Headers to set", "cacheControl", "contentDisposition" },
|
||||
{ "T3", "id", "createDate", "modifyDate" } }),
|
||||
Map.entry(PROTECTION_RULE, new String[][] {
|
||||
{ "T1", "bucketId", "prefixGlob", "mode", "enabled" },
|
||||
{ "T2:Why", "reason", "allowedSystemCallers" },
|
||||
{ "T3", "id", "createDate", "modifyDate" } }),
|
||||
Map.entry(OPERATION, new String[][] {
|
||||
{ "T1", "occurredAt", "action", "bucketAlias", "objectKey", "destinationKey", "result", "actor" },
|
||||
{ "T2:Detail", "bytes", "errorMessage", "providerRequestId", "processRunId", "detail", "bucketId" },
|
||||
{ "T3", "id", "createDate", "modifyDate" } }),
|
||||
Map.entry(USAGE_DAILY, new String[][] {
|
||||
{ "T1", "bucketId", "snapshotDate", "objectCount", "totalBytes" },
|
||||
{ "T2:By top-level folder", "byPrefix" },
|
||||
{ "T3", "id", "createDate", "modifyDate" } }),
|
||||
Map.entry(CONNECTION_CAPABILITY, new String[][] {
|
||||
{ "T1", "connectionId", "capability", "status", "source", "probedAt" },
|
||||
{ "T2:Detail", "detail" },
|
||||
{ "T3", "id", "createDate", "modifyDate" } }),
|
||||
Map.entry(BUCKET_CAPABILITY, new String[][] {
|
||||
{ "T1", "bucketId", "capability", "status", "source", "probedAt" },
|
||||
{ "T2:Detail", "detail" },
|
||||
{ "T3", "id", "createDate", "modifyDate" } }));
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Give a table its record-screen sections (identity first, then detail groups,
|
||||
** then bookkeeping) and take the buttons away from tables that change only
|
||||
** through actions or from code config.
|
||||
*******************************************************************************/
|
||||
private static void layout(QTableMetaData table)
|
||||
{
|
||||
String[][] rows = LAYOUTS.get(table.getName());
|
||||
if(rows != null)
|
||||
{
|
||||
for(String[] row : rows)
|
||||
{
|
||||
String kind = row[0];
|
||||
String[] fields = java.util.Arrays.copyOfRange(row, 1, row.length);
|
||||
if(kind.equals("T1"))
|
||||
{
|
||||
table.withSection(SectionFactory.defaultT1(fields));
|
||||
}
|
||||
else if(kind.equals("T3"))
|
||||
{
|
||||
table.withSection(SectionFactory.defaultT3(fields));
|
||||
}
|
||||
else
|
||||
{
|
||||
String label = kind.substring(kind.indexOf(':') + 1);
|
||||
table.withSection(new QFieldSection(label.toLowerCase().replaceAll("[^a-z0-9]+", "_"), label, new QIcon("list"), Tier.T2, List.of(fields)));
|
||||
}
|
||||
}
|
||||
}
|
||||
if(READ_ONLY.contains(table.getName()))
|
||||
{
|
||||
table.withoutCapabilities(Capability.TABLE_INSERT, Capability.TABLE_UPDATE, Capability.TABLE_DELETE);
|
||||
}
|
||||
else if(CONNECTION.equals(table.getName()) || BUCKET.equals(table.getName()))
|
||||
{
|
||||
table.withoutCapabilities(Capability.TABLE_INSERT, Capability.TABLE_DELETE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The join metadata for every parent/child relation.
|
||||
*******************************************************************************/
|
||||
public static List<QJoinMetaData> joins()
|
||||
{
|
||||
List<QJoinMetaData> joins = new ArrayList<>();
|
||||
for(String[] relation : RELATIONS)
|
||||
{
|
||||
joins.add(new QJoinMetaData().withName(joinName(relation)).withLeftTable(relation[0]).withRightTable(relation[1]).withType(JoinType.ONE_TO_MANY)
|
||||
.withJoinOn(new JoinOn("id", relation[2])));
|
||||
}
|
||||
return (joins);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The child record lists shown on a parent's record screen: a bucket shows its
|
||||
** folders, objects, references, usage, operations and rules; a folder and an
|
||||
** object show what is beneath or about them.
|
||||
*******************************************************************************/
|
||||
public static List<QWidgetMetaData> childListWidgets()
|
||||
{
|
||||
List<QWidgetMetaData> widgets = new ArrayList<>();
|
||||
for(String[] relation : RELATIONS)
|
||||
{
|
||||
QJoinMetaData join = new QJoinMetaData().withName(joinName(relation)).withLeftTable(relation[0]).withRightTable(relation[1]).withType(JoinType.ONE_TO_MANY)
|
||||
.withJoinOn(new JoinOn("id", relation[2]));
|
||||
ChildRecordListRenderer.Builder builder = ChildRecordListRenderer.widgetMetaDataBuilder(join).withName(joinName(relation)).withLabel(relation[3]).withMaxRows(10);
|
||||
if(CHILD_LIST_COLUMNS.containsKey(relation[1]))
|
||||
{
|
||||
builder.withOnlyIncludeFieldNames(CHILD_LIST_COLUMNS.get(relation[1]));
|
||||
}
|
||||
widgets.add(builder.getWidgetMetaData());
|
||||
}
|
||||
return (widgets);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A unique join name per relation.
|
||||
*******************************************************************************/
|
||||
private static String joinName(String[] relation)
|
||||
{
|
||||
return (relation[0] + "_" + relation[2] + "_" + relation[1]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A table with the common id/createDate/modifyDate fields.
|
||||
*******************************************************************************/
|
||||
private static QTableMetaData base(ObjectStorageQBitConfig config, String name, String label, String icon)
|
||||
{
|
||||
return (new QTableMetaData()
|
||||
.withName(name)
|
||||
.withLabel(label)
|
||||
.withIcon(new QIcon(icon))
|
||||
.withBackendName(config.getBackendName())
|
||||
.withPrimaryKeyField("id")
|
||||
.withPermissionRules(permissionRules())
|
||||
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
|
||||
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false))
|
||||
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A read-only field.
|
||||
*******************************************************************************/
|
||||
private static QFieldMetaData ro(String name, QFieldType type)
|
||||
{
|
||||
return (new QFieldMetaData(name, type).withIsEditable(false));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A read-only string field with a max length.
|
||||
*******************************************************************************/
|
||||
private static QFieldMetaData ros(String name, int max)
|
||||
{
|
||||
return (ro(name, QFieldType.STRING).withMaxLength(max));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** An editable string field.
|
||||
*******************************************************************************/
|
||||
private static QFieldMetaData str(String name, int max)
|
||||
{
|
||||
return (new QFieldMetaData(name, QFieldType.STRING).withMaxLength(max));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Apply a record security lock on a field (when scoping is on).
|
||||
*******************************************************************************/
|
||||
private static QTableMetaData lock(ObjectStorageQBitConfig config, QTableMetaData table, String keyType, String fieldName, boolean nullAllowed)
|
||||
{
|
||||
if(Boolean.TRUE.equals(config.getBucketScoping()))
|
||||
{
|
||||
table.withRecordSecurityLock(new RecordSecurityLock().withSecurityKeyType(keyType).withFieldName(fieldName)
|
||||
.withNullValueBehavior(nullAllowed ? RecordSecurityLock.NullValueBehavior.ALLOW : RecordSecurityLock.NullValueBehavior.DENY));
|
||||
}
|
||||
return (table);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** object_storage_connection: one S3 endpoint and its settings. Identity
|
||||
** fields come from code config and are read-only; only operational fields are
|
||||
** editable (with object_storage.admin).
|
||||
*******************************************************************************/
|
||||
static QTableMetaData connection(ObjectStorageQBitConfig config)
|
||||
{
|
||||
return (lock(config, base(config, CONNECTION, "Storage Connection", "cloud")
|
||||
.withRecordLabelFormatAndFields("%s", "name")
|
||||
.withField(ros("name", 100))
|
||||
.withField(ro("source", QFieldType.STRING).withPossibleValueSourceName(PVS_SOURCE))
|
||||
.withField(ro("providerType", QFieldType.STRING).withPossibleValueSourceName(PVS_PROVIDER_TYPE))
|
||||
.withField(ros("endpoint", 500))
|
||||
.withField(ros("presignEndpoint", 500))
|
||||
.withField(ros("region", 100))
|
||||
.withField(ros("addressingStyle", 20))
|
||||
.withField(ros("signatureVersion", 10))
|
||||
.withField(ros("checksumMode", 20))
|
||||
.withField(ros("payloadSigning", 30))
|
||||
.withField(ro("tlsVerify", QFieldType.BOOLEAN))
|
||||
.withField(ros("credentialsMode", 20))
|
||||
.withField(ros("accessKeyRef", 200))
|
||||
.withField(ros("secretKeyRef", 200))
|
||||
.withField(ros("presignAccessKeyRef", 200))
|
||||
.withField(ros("presignSecretKeyRef", 200))
|
||||
.withField(ro("credentialsResolved", QFieldType.BOOLEAN))
|
||||
.withField(ro("presignKeyConfigured", QFieldType.BOOLEAN))
|
||||
.withField(ro("presignWarning", QFieldType.STRING).withMaxLength(300))
|
||||
.withField(ros("adminApiType", 20))
|
||||
.withField(new QFieldMetaData("maxOpsPerSecond", QFieldType.INTEGER).withDefaultValue(20))
|
||||
.withField(new QFieldMetaData("maxConcurrency", QFieldType.INTEGER).withDefaultValue(4))
|
||||
.withField(new QFieldMetaData("connectTimeoutMs", QFieldType.INTEGER).withDefaultValue(5000))
|
||||
.withField(new QFieldMetaData("readTimeoutMs", QFieldType.INTEGER).withDefaultValue(60000))
|
||||
.withField(ro("lastTestedAt", QFieldType.DATE_TIME))
|
||||
.withField(ros("lastTestStatus", 30))
|
||||
.withField(ro("lastTestSummary", QFieldType.TEXT))
|
||||
.withField(ro("clockSkewSeconds", QFieldType.INTEGER))
|
||||
.withField(ros("settingsHash", 40)), KEY_TYPE_CONNECTION, "id", false));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** object_storage_connection_capability.
|
||||
*******************************************************************************/
|
||||
static QTableMetaData connectionCapability(ObjectStorageQBitConfig config)
|
||||
{
|
||||
return (lock(config, base(config, CONNECTION_CAPABILITY, "Storage Connection Capability", "fact_check")
|
||||
.withRecordLabelFormatAndFields("%s", "capability")
|
||||
.withField(ro("connectionId", QFieldType.INTEGER).withPossibleValueSourceName(CONNECTION))
|
||||
.withField(ro("capability", QFieldType.STRING).withPossibleValueSourceName(PVS_CAPABILITY))
|
||||
.withField(ro("status", QFieldType.STRING).withPossibleValueSourceName(PVS_CAP_STATUS))
|
||||
.withField(ro("detail", QFieldType.TEXT))
|
||||
.withField(ro("source", QFieldType.STRING).withPossibleValueSourceName(PVS_CAP_SOURCE))
|
||||
.withField(ro("probedAt", QFieldType.DATE_TIME)), KEY_TYPE_CONNECTION, "connectionId", false));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** object_storage_bucket: an alias, its config, and the observed state.
|
||||
*******************************************************************************/
|
||||
static QTableMetaData bucket(ObjectStorageQBitConfig config)
|
||||
{
|
||||
return (lock(config, base(config, BUCKET, "Storage Bucket", "inventory_2")
|
||||
.withRecordLabelFormatAndFields("%s", "alias")
|
||||
.withField(ros("alias", 100))
|
||||
.withField(ro("connectionId", QFieldType.INTEGER).withPossibleValueSourceName(CONNECTION))
|
||||
.withField(ros("bucketName", 200))
|
||||
.withField(ro("source", QFieldType.STRING).withPossibleValueSourceName(PVS_SOURCE))
|
||||
.withField(ro("role", QFieldType.STRING).withPossibleValueSourceName(PVS_BUCKET_ROLE))
|
||||
.withField(ro("publicBaseUrl", QFieldType.STRING).withMaxLength(500))
|
||||
.withField(ro("indexed", QFieldType.BOOLEAN))
|
||||
.withField(ro("managedBy", QFieldType.STRING).withPossibleValueSourceName(PVS_MANAGED_BY))
|
||||
.withField(ros("trashBucketAlias", 100))
|
||||
.withField(ros("trashPrefix", 200))
|
||||
.withField(new QFieldMetaData("maxPresignGetSeconds", QFieldType.INTEGER).withDefaultValue(3600))
|
||||
.withField(new QFieldMetaData("maxPresignPutSeconds", QFieldType.INTEGER).withDefaultValue(900))
|
||||
.withField(ro("referenceTracked", QFieldType.BOOLEAN))
|
||||
.withField(new QFieldMetaData("orphanCountAlertThreshold", QFieldType.INTEGER))
|
||||
.withField(new QFieldMetaData("quotaBytes", QFieldType.LONG))
|
||||
.withField(ro("versioningState", QFieldType.STRING).withMaxLength(30))
|
||||
.withField(ro("lifecycleSummary", QFieldType.TEXT))
|
||||
.withField(ro("policySummary", QFieldType.TEXT))
|
||||
.withField(ro("corsSummary", QFieldType.TEXT))
|
||||
.withField(ro("configDriftNote", QFieldType.TEXT))
|
||||
.withField(ro("objectCount", QFieldType.LONG))
|
||||
.withField(ro("totalBytes", QFieldType.LONG))
|
||||
.withField(ro("lastScanDate", QFieldType.DATE_TIME))
|
||||
.withField(ros("lastScanStatus", 30))
|
||||
.withField(ro("scanGeneration", QFieldType.LONG))
|
||||
.withField(ro("scanCursor", QFieldType.STRING).withMaxLength(1100))
|
||||
.withField(ro("headersKnownCount", QFieldType.LONG))
|
||||
.withField(ro("headersMatchingPolicyCount", QFieldType.LONG))
|
||||
.withField(ro("orphanedCount", QFieldType.LONG))
|
||||
.withField(ro("orphanedBytes", QFieldType.LONG))
|
||||
.withField(ro("missingCount", QFieldType.LONG))
|
||||
.withField(ro("lastReferenceScanDate", QFieldType.DATE_TIME)), KEY_TYPE_BUCKET, "id", false));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** object_storage_bucket_capability.
|
||||
*******************************************************************************/
|
||||
static QTableMetaData bucketCapability(ObjectStorageQBitConfig config)
|
||||
{
|
||||
return (lock(config, base(config, BUCKET_CAPABILITY, "Storage Bucket Capability", "fact_check")
|
||||
.withRecordLabelFormatAndFields("%s", "capability")
|
||||
.withField(ro("bucketId", QFieldType.INTEGER).withPossibleValueSourceName(BUCKET))
|
||||
.withField(ro("capability", QFieldType.STRING).withPossibleValueSourceName(PVS_CAPABILITY))
|
||||
.withField(ro("status", QFieldType.STRING).withPossibleValueSourceName(PVS_CAP_STATUS))
|
||||
.withField(ro("detail", QFieldType.TEXT))
|
||||
.withField(ro("source", QFieldType.STRING).withPossibleValueSourceName(PVS_CAP_SOURCE))
|
||||
.withField(ro("probedAt", QFieldType.DATE_TIME)), KEY_TYPE_BUCKET, "bucketId", false));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** object_storage_folder: a prefix in an indexed bucket, rolled up.
|
||||
*******************************************************************************/
|
||||
static QTableMetaData folder(ObjectStorageQBitConfig config)
|
||||
{
|
||||
return (lock(config, base(config, FOLDER, "Storage Folder", "folder")
|
||||
.withRecordLabelFormatAndFields("%s", "path")
|
||||
.withField(ro("bucketId", QFieldType.INTEGER).withPossibleValueSourceName(BUCKET))
|
||||
.withField(ro("parentFolderId", QFieldType.INTEGER).withPossibleValueSourceName(FOLDER))
|
||||
.withField(ros("path", 1100))
|
||||
.withField(ros("name", 300))
|
||||
.withField(ro("objectCount", QFieldType.LONG))
|
||||
.withField(ro("totalBytes", QFieldType.LONG))
|
||||
.withField(ro("newestChange", QFieldType.DATE_TIME)), KEY_TYPE_BUCKET, "bucketId", false));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** object_storage_object: one object in an indexed bucket. Mirrors S3 and is
|
||||
** read-only; changes go through actions.
|
||||
*******************************************************************************/
|
||||
static QTableMetaData object(ObjectStorageQBitConfig config)
|
||||
{
|
||||
return (lock(config, base(config, OBJECT, "Storage Object", "description")
|
||||
.withRecordLabelFormatAndFields("%s", "objectKey")
|
||||
.withField(ro("bucketId", QFieldType.INTEGER).withPossibleValueSourceName(BUCKET))
|
||||
.withField(ro("folderId", QFieldType.INTEGER).withPossibleValueSourceName(FOLDER))
|
||||
.withField(ros("objectKey", 1100))
|
||||
.withField(ros("name", 300))
|
||||
.withField(ros("extension", 30))
|
||||
.withField(ro("size", QFieldType.LONG))
|
||||
.withField(ros("contentType", 200))
|
||||
.withField(ros("etag", 100))
|
||||
.withField(ro("lastModified", QFieldType.DATE_TIME))
|
||||
.withField(ros("storageClass", 40))
|
||||
.withField(ros("cacheControl", 300))
|
||||
.withField(ros("contentDisposition", 300))
|
||||
.withField(ro("userMetadata", QFieldType.TEXT))
|
||||
.withField(ro("tags", QFieldType.TEXT))
|
||||
.withField(ros("versionId", 200))
|
||||
.withField(ro("referenceCount", QFieldType.INTEGER))
|
||||
.withField(ros("protection", 30))
|
||||
.withField(ro("headersKnown", QFieldType.BOOLEAN))
|
||||
.withField(ro("matchesHeaderPolicy", QFieldType.BOOLEAN))
|
||||
.withField(ro("deleted", QFieldType.BOOLEAN))
|
||||
.withField(ro("scanGeneration", QFieldType.LONG))
|
||||
.withField(ro("indexedAt", QFieldType.DATE_TIME))
|
||||
.withField(ro("previewUrl", QFieldType.STRING).withMaxLength(2000).withLabel("Preview")
|
||||
.withFieldAdornment(AdornmentType.FilePreviewValues.newFieldAdornment()
|
||||
.withValue(AdornmentType.FilePreviewValues.FILE_NAME_FIELD, "name")
|
||||
.withValue(AdornmentType.FilePreviewValues.MIME_TYPE_FIELD, "contentType")
|
||||
.withValue(AdornmentType.FilePreviewValues.SIZE_BYTES_FIELD, "size")
|
||||
.withValue(AdornmentType.FilePreviewValues.THUMBNAIL_MAX_SIZE, 48)))
|
||||
.withCustomizer(TableCustomizers.POST_QUERY_RECORD, new QCodeReference(ObjectPreviewCustomizer.class)), KEY_TYPE_BUCKET, "bucketId", false));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** object_storage_object_version: a non-current version or delete marker.
|
||||
*******************************************************************************/
|
||||
static QTableMetaData objectVersion(ObjectStorageQBitConfig config)
|
||||
{
|
||||
return (lock(config, base(config, OBJECT_VERSION, "Storage Object Version", "history")
|
||||
.withRecordLabelFormatAndFields("%s", "objectKey")
|
||||
.withField(ro("bucketId", QFieldType.INTEGER).withPossibleValueSourceName(BUCKET))
|
||||
.withField(ro("objectId", QFieldType.INTEGER).withPossibleValueSourceName(OBJECT))
|
||||
.withField(ros("objectKey", 1100))
|
||||
.withField(ros("versionId", 200))
|
||||
.withField(ro("isDeleteMarker", QFieldType.BOOLEAN))
|
||||
.withField(ro("isLatest", QFieldType.BOOLEAN))
|
||||
.withField(ro("size", QFieldType.LONG))
|
||||
.withField(ro("lastModified", QFieldType.DATE_TIME)), KEY_TYPE_BUCKET, "bucketId", false));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** object_storage_object_reference: a record that points at a key or prefix.
|
||||
** objectId is null when the referenced object does not exist (missing).
|
||||
*******************************************************************************/
|
||||
static QTableMetaData objectReference(ObjectStorageQBitConfig config)
|
||||
{
|
||||
return (lock(config, base(config, OBJECT_REFERENCE, "Storage Object Reference", "link")
|
||||
.withRecordLabelFormatAndFields("%s", "label")
|
||||
.withField(ro("bucketId", QFieldType.INTEGER).withPossibleValueSourceName(BUCKET))
|
||||
.withField(ro("objectId", QFieldType.INTEGER).withPossibleValueSourceName(OBJECT))
|
||||
.withField(ros("keyOrPrefix", 1100))
|
||||
.withField(ros("matchMode", 10))
|
||||
.withField(ros("sourceTable", 100))
|
||||
.withField(ros("sourceRecordId", 100))
|
||||
.withField(ros("sourceField", 100))
|
||||
.withField(ros("label", 300))
|
||||
.withField(ro("missing", QFieldType.BOOLEAN))
|
||||
.withField(ro("scannedAt", QFieldType.DATE_TIME)), KEY_TYPE_BUCKET, "bucketId", false));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** object_storage_header_rule: header policy rules (ordinary audited data).
|
||||
*******************************************************************************/
|
||||
static QTableMetaData headerRule(ObjectStorageQBitConfig config)
|
||||
{
|
||||
return (lock(config, base(config, HEADER_RULE, "Storage Header Rule", "rule")
|
||||
.withRecordLabelFormatAndFields("%s", "name")
|
||||
.withField(str("name", 100).withIsRequired(true))
|
||||
.withField(new QFieldMetaData("bucketId", QFieldType.INTEGER).withPossibleValueSourceName(BUCKET))
|
||||
.withField(str("keyGlob", 500))
|
||||
.withField(str("keyRegex", 500))
|
||||
.withField(str("contentTypePattern", 200))
|
||||
.withField(str("cacheControl", 300))
|
||||
.withField(str("contentDisposition", 300))
|
||||
.withField(new QFieldMetaData("priority", QFieldType.INTEGER).withDefaultValue(100))
|
||||
.withField(new QFieldMetaData("enabled", QFieldType.BOOLEAN).withDefaultValue(true)), KEY_TYPE_BUCKET, "bucketId", true));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** object_storage_protection_rule: a protected bucket/prefix.
|
||||
*******************************************************************************/
|
||||
static QTableMetaData protectionRule(ObjectStorageQBitConfig config)
|
||||
{
|
||||
return (lock(config, base(config, PROTECTION_RULE, "Storage Protection Rule", "shield")
|
||||
.withRecordLabelFormatAndFields("%s", "prefixGlob")
|
||||
.withField(new QFieldMetaData("bucketId", QFieldType.INTEGER).withPossibleValueSourceName(BUCKET).withIsRequired(true))
|
||||
.withField(str("prefixGlob", 500).withIsRequired(true))
|
||||
.withField(new QFieldMetaData("mode", QFieldType.STRING).withPossibleValueSourceName(PVS_PROTECTION).withIsRequired(true).withDefaultValue("NO_DELETE"))
|
||||
.withField(str("reason", 500))
|
||||
.withField(str("allowedSystemCallers", 300))
|
||||
.withField(new QFieldMetaData("enabled", QFieldType.BOOLEAN).withDefaultValue(true)), KEY_TYPE_BUCKET, "bucketId", false));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** object_storage_operation: the storage audit log, written only by the service.
|
||||
*******************************************************************************/
|
||||
static QTableMetaData operation(ObjectStorageQBitConfig config)
|
||||
{
|
||||
return (lock(config, base(config, OPERATION, "Storage Operation", "history")
|
||||
.withRecordLabelFormatAndFields("%s %s", "action", "objectKey")
|
||||
.withField(ro("occurredAt", QFieldType.DATE_TIME))
|
||||
.withField(ros("actor", 300))
|
||||
.withField(ro("action", QFieldType.STRING).withPossibleValueSourceName(PVS_OPERATION))
|
||||
.withField(ro("bucketId", QFieldType.INTEGER).withPossibleValueSourceName(BUCKET))
|
||||
.withField(ros("bucketAlias", 100))
|
||||
.withField(ros("objectKey", 1100))
|
||||
.withField(ros("destinationKey", 1100))
|
||||
.withField(ro("bytes", QFieldType.LONG))
|
||||
.withField(ro("result", QFieldType.STRING).withPossibleValueSourceName(PVS_RESULT))
|
||||
.withField(ro("errorMessage", QFieldType.TEXT))
|
||||
.withField(ros("providerRequestId", 100))
|
||||
.withField(ro("processRunId", QFieldType.STRING).withMaxLength(100))
|
||||
.withField(ro("detail", QFieldType.TEXT)), KEY_TYPE_BUCKET, "bucketId", true));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** object_storage_usage_daily: one snapshot per bucket per day.
|
||||
*******************************************************************************/
|
||||
static QTableMetaData usageDaily(ObjectStorageQBitConfig config)
|
||||
{
|
||||
return (lock(config, base(config, USAGE_DAILY, "Storage Usage", "insights")
|
||||
.withRecordLabelFormatAndFields("%s", "snapshotDate")
|
||||
.withField(ro("bucketId", QFieldType.INTEGER).withPossibleValueSourceName(BUCKET))
|
||||
.withField(ro("snapshotDate", QFieldType.DATE))
|
||||
.withField(ro("objectCount", QFieldType.LONG))
|
||||
.withField(ro("totalBytes", QFieldType.LONG))
|
||||
.withField(ro("byPrefix", QFieldType.TEXT)), KEY_TYPE_BUCKET, "bucketId", false));
|
||||
}
|
||||
}
|
||||
-117
@@ -1,117 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.permissions;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.objectstorage.internal.ObjectStorageRuntime;
|
||||
import com.kingsrook.qbits.objectstorage.model.ObjectStorageTables;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.CustomPermissionChecker;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** One checker for every qbit table, process, app and widget:
|
||||
** - table reads, the app and widgets need object_storage.view;
|
||||
** - writes to the editable config tables (connection operational fields, header
|
||||
** rules, protection rules, bucket thresholds) need object_storage.admin;
|
||||
** observed and history tables are never written through the UI or API (they
|
||||
** change only through actions and the service);
|
||||
** - each process needs the permission registered for it (default admin).
|
||||
*******************************************************************************/
|
||||
public class ObjectStoragePermissionChecker implements CustomPermissionChecker
|
||||
{
|
||||
private static final List<String> ADMIN_EDITABLE = List.of(ObjectStorageTables.CONNECTION, ObjectStorageTables.BUCKET,
|
||||
ObjectStorageTables.HEADER_RULE, ObjectStorageTables.PROTECTION_RULE);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Check, throwing when denied.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void checkPermissionsThrowing(AbstractActionInput actionInput, MetaDataWithPermissionRules metaData) throws QPermissionDeniedException
|
||||
{
|
||||
String needed = required(actionInput, metaData);
|
||||
if(needed == null)
|
||||
{
|
||||
throw (new QPermissionDeniedException("This storage table changes only through storage actions."));
|
||||
}
|
||||
if(!has(needed))
|
||||
{
|
||||
throw (new QPermissionDeniedException("This needs the " + needed + " permission."));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The permission an action needs, or null when nobody may do it directly.
|
||||
*******************************************************************************/
|
||||
static String required(AbstractActionInput actionInput, MetaDataWithPermissionRules metaData)
|
||||
{
|
||||
boolean write = actionInput instanceof InsertInput || actionInput instanceof UpdateInput || actionInput instanceof DeleteInput;
|
||||
if(metaData instanceof QTableMetaData table)
|
||||
{
|
||||
if(!write)
|
||||
{
|
||||
return (ObjectStoragePermissions.VIEW);
|
||||
}
|
||||
return (ADMIN_EDITABLE.contains(table.getName()) ? ObjectStoragePermissions.ADMIN : null);
|
||||
}
|
||||
if(metaData instanceof QProcessMetaData process)
|
||||
{
|
||||
Map<String, String> byProcess = ObjectStorageRuntime.processPermissions();
|
||||
return (byProcess.getOrDefault(process.getName(), ObjectStoragePermissions.ADMIN));
|
||||
}
|
||||
return (ObjectStoragePermissions.VIEW);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether the current session holds a permission (admin implies the others),
|
||||
** or the instance is fully anonymous and the config allows that (local dev).
|
||||
*******************************************************************************/
|
||||
public static boolean has(String permission)
|
||||
{
|
||||
QInstance instance = QContext.getQInstance();
|
||||
QSession session = QContext.getQSession();
|
||||
if(session != null && (session.hasPermission(permission) || session.hasPermission(ObjectStoragePermissions.ADMIN)))
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
return (ObjectStorageRuntime.isInstalled() && Boolean.TRUE.equals(ObjectStorageRuntime.getConfig().getOpenWhenAnonymous())
|
||||
&& instance != null && instance.getAuthentication() != null && QAuthenticationType.FULLY_ANONYMOUS.equals(instance.getAuthentication().getType()));
|
||||
}
|
||||
}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.permissions;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The permission names. object_storage.admin implies the others.
|
||||
*******************************************************************************/
|
||||
public final class ObjectStoragePermissions
|
||||
{
|
||||
/** Read all storage tables; preview, download, presign GET. */
|
||||
public static final String VIEW = "object_storage.view";
|
||||
/** Upload, copy, move/rename, change headers and metadata, apply header policy, presign PUT, restore. */
|
||||
public static final String WRITE = "object_storage.write";
|
||||
/** Delete objects (soft where possible). */
|
||||
public static final String DELETE = "object_storage.delete";
|
||||
/** Edit connections (operational fields), header rules, protection rules; write probes; permanent deletes; runs over the bulk cap. */
|
||||
public static final String ADMIN = "object_storage.admin";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private ObjectStoragePermissions()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,292 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.processes;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.objectstorage.api.ObjectStorage;
|
||||
import com.kingsrook.qbits.objectstorage.api.StorageBucket;
|
||||
import com.kingsrook.qbits.objectstorage.api.StorageCaller;
|
||||
import com.kingsrook.qbits.objectstorage.api.StoredObject;
|
||||
import com.kingsrook.qbits.objectstorage.config.StorageBucketConfig;
|
||||
import com.kingsrook.qbits.objectstorage.internal.ConfigSync;
|
||||
import com.kingsrook.qbits.objectstorage.internal.Db;
|
||||
import com.kingsrook.qbits.objectstorage.internal.ObjectStorageRuntime;
|
||||
import com.kingsrook.qbits.objectstorage.internal.ProtectionRules;
|
||||
import com.kingsrook.qbits.objectstorage.model.ObjectStorageTables;
|
||||
import com.kingsrook.qbits.objectstorage.permissions.ObjectStoragePermissionChecker;
|
||||
import com.kingsrook.qbits.objectstorage.permissions.ObjectStoragePermissions;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Shared helpers for the record and table actions: resolving a row's bucket,
|
||||
** the skip rules every destructive action applies (referenced, protected,
|
||||
** object lock), permission re-checks, and per-run caches.
|
||||
*******************************************************************************/
|
||||
public final class ActionSupport
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Why an action leaves an object alone.
|
||||
*******************************************************************************/
|
||||
public enum Skip
|
||||
{
|
||||
NONE,
|
||||
REFERENCED,
|
||||
PROTECTED,
|
||||
LOCKED,
|
||||
BACKUP_BUCKET,
|
||||
NO_BUCKET_ACCESS
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A classification: the reason, and a human detail for the review line.
|
||||
*******************************************************************************/
|
||||
public record Classification(Skip skip, String detail)
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Nothing in the way.
|
||||
*******************************************************************************/
|
||||
public static Classification none()
|
||||
{
|
||||
return (new Classification(Skip.NONE, null));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private ActionSupport()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The alias of the bucket an object, folder or reference row belongs to.
|
||||
*******************************************************************************/
|
||||
public static String aliasOf(QRecord row)
|
||||
{
|
||||
Integer bucketId = row.getValueInteger("bucketId");
|
||||
if(bucketId == null && row.getValueString("alias") != null)
|
||||
{
|
||||
return (row.getValueString("alias"));
|
||||
}
|
||||
String alias = ConfigSync.aliasOf(bucketId);
|
||||
if(alias == null && bucketId != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
QRecord bucket = Db.asSystem(() -> Db.first(ObjectStorageTables.BUCKET, "id", bucketId));
|
||||
alias = bucket == null ? null : bucket.getValueString("alias");
|
||||
}
|
||||
catch(QException e)
|
||||
{
|
||||
alias = null;
|
||||
}
|
||||
}
|
||||
return (alias);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The configured bucket (null when the row's bucket is not in this config).
|
||||
*******************************************************************************/
|
||||
public static StorageBucketConfig config(String alias)
|
||||
{
|
||||
return (alias == null ? null : ObjectStorageRuntime.getConfig().findBucket(alias));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether the current user holds a permission (admin implies the rest).
|
||||
*******************************************************************************/
|
||||
public static boolean has(String permission)
|
||||
{
|
||||
return (ObjectStoragePermissionChecker.has(permission));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether the current user is an admin.
|
||||
*******************************************************************************/
|
||||
public static boolean isAdmin()
|
||||
{
|
||||
return (has(ObjectStoragePermissions.ADMIN));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether the current user can see a bucket row (the per-bucket record lock is
|
||||
** applied by the query). The load step uses this before each provider call.
|
||||
*******************************************************************************/
|
||||
public static boolean canSeeBucket(Integer bucketId, Map<Integer, Boolean> cache)
|
||||
{
|
||||
if(bucketId == null)
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
return (cache.computeIfAbsent(bucketId, id ->
|
||||
{
|
||||
try
|
||||
{
|
||||
return (!new QueryAction().execute(new QueryInput(ObjectStorageTables.BUCKET).withFilter(new com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter(Db.eq("id", id)))).getRecords().isEmpty());
|
||||
}
|
||||
catch(QException e)
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The service handle for a bucket alias, as the current user.
|
||||
*******************************************************************************/
|
||||
public static StorageBucket storage(String alias)
|
||||
{
|
||||
return (ObjectStorage.bucket(alias));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Classify an object for a destructive action: BACKUP buckets are outside the
|
||||
** actions (backups manage their own objects); a referenced object is skipped
|
||||
** when asked; protection rules apply; an object under lock is skipped.
|
||||
*******************************************************************************/
|
||||
public static Classification classify(QRecord row, ProtectionRules.Op op, boolean checkReferences, boolean checkLock)
|
||||
{
|
||||
String alias = aliasOf(row);
|
||||
StorageBucketConfig config = config(alias);
|
||||
if(config == null)
|
||||
{
|
||||
return (new Classification(Skip.NO_BUCKET_ACCESS, "its bucket is not in the configuration"));
|
||||
}
|
||||
if(config.getRole() == com.kingsrook.qbits.objectstorage.config.BucketRole.BACKUP)
|
||||
{
|
||||
return (new Classification(Skip.BACKUP_BUCKET, "backup buckets are managed by the backups qbit"));
|
||||
}
|
||||
if(checkReferences && row.getValueInteger("referenceCount") != null && row.getValueInteger("referenceCount") > 0)
|
||||
{
|
||||
return (new Classification(Skip.REFERENCED, "referenced by " + referencedBy(row)));
|
||||
}
|
||||
ProtectionRules.Result protection = ProtectionRules.check(alias, row.getValueString("objectKey"), op, StorageCaller.user());
|
||||
if(protection.blocked())
|
||||
{
|
||||
return (new Classification(Skip.PROTECTED, protection.reason()));
|
||||
}
|
||||
if(checkLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
StoredObject head = storage(alias).head(row.getValueString("objectKey")).orElse(null);
|
||||
if(head != null && head.getLockRetainUntil() != null && head.getLockRetainUntil().isAfter(Instant.now()))
|
||||
{
|
||||
return (new Classification(Skip.LOCKED, "object lock until " + head.getLockRetainUntil()));
|
||||
}
|
||||
}
|
||||
catch(RuntimeException e)
|
||||
{
|
||||
return (Classification.none());
|
||||
}
|
||||
}
|
||||
return (Classification.none());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The first few records that reference an object, for the review line.
|
||||
*******************************************************************************/
|
||||
public static String referencedBy(QRecord row)
|
||||
{
|
||||
try
|
||||
{
|
||||
List<QRecord> refs = Db.asSystem(() -> Db.query(ObjectStorageTables.OBJECT_REFERENCE, "objectId", row.getValue("id")));
|
||||
List<String> labels = new ArrayList<>();
|
||||
for(QRecord ref : refs)
|
||||
{
|
||||
labels.add(StringUtils.hasContent(ref.getValueString("label")) ? ref.getValueString("label") : ref.getValueString("sourceTable") + " #" + ref.getValueString("sourceRecordId"));
|
||||
if(labels.size() == 3)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
int count = row.getValueInteger("referenceCount") == null ? refs.size() : row.getValueInteger("referenceCount");
|
||||
return (count + " record" + (count == 1 ? "" : "s") + (labels.isEmpty() ? "" : " (" + String.join(", ", labels) + (count > labels.size() ? ", ..." : "") + ")"));
|
||||
}
|
||||
catch(QException e)
|
||||
{
|
||||
return ("other records");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The object rows of a run, loaded by id when only a partial record came in.
|
||||
*******************************************************************************/
|
||||
public static Map<String, Serializable> noValues()
|
||||
{
|
||||
return (new HashMap<>());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Bytes as KB, MB, GB.
|
||||
*******************************************************************************/
|
||||
public static String bytes(long value)
|
||||
{
|
||||
if(value < 1024)
|
||||
{
|
||||
return (value + " B");
|
||||
}
|
||||
String[] units = { "KB", "MB", "GB", "TB" };
|
||||
double v = value;
|
||||
int u = -1;
|
||||
while(v >= 1024 && u < units.length - 1)
|
||||
{
|
||||
v /= 1024;
|
||||
u++;
|
||||
}
|
||||
return (String.format("%.1f %s", v, units[u]));
|
||||
}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.processes;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.objectstorage.api.StorageException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.Status;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractLoadStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ProcessSummaryProviderInterface;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The execute half of an action. Before each provider call it re-checks the
|
||||
** per-bucket lock for the record (defense in depth: the review already filtered,
|
||||
** but the state may have changed). A failure on one record is counted and listed
|
||||
** with the error; it never stops the others. Every write lands in
|
||||
** object_storage_operation through the service.
|
||||
*******************************************************************************/
|
||||
public abstract class BaseLoad extends AbstractLoadStep implements ProcessSummaryProviderInterface
|
||||
{
|
||||
protected final Map<Integer, Boolean> bucketAccess = new HashMap<>();
|
||||
|
||||
protected final ProcessSummaryLine failed = EtlSupport.line(Status.ERROR, "record failed (see Operations for the error)", "records failed (see Operations for the errors)",
|
||||
"record failed (see Operations for the error)", "records failed (see Operations for the errors)");
|
||||
|
||||
private final ArrayList<String> failures = new ArrayList<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The bucket row id a record belongs to (the record's bucketId by default).
|
||||
*******************************************************************************/
|
||||
protected Integer bucketIdOf(QRecord record)
|
||||
{
|
||||
return (record.getValueInteger("bucketId"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Do the work for one record.
|
||||
*******************************************************************************/
|
||||
protected abstract void apply(QRecord record, RunBackendStepInput input, RunBackendStepOutput output) throws QException;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Add this action's own result lines.
|
||||
*******************************************************************************/
|
||||
protected abstract void addLines(ArrayList<ProcessSummaryLineInterface> lines);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Re-check the bucket lock, run the work, count failures.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public final void runOnePage(RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
for(QRecord record : input.getRecords())
|
||||
{
|
||||
Integer bucketId = bucketIdOf(record);
|
||||
if(bucketId != null && !ActionSupport.canSeeBucket(bucketId, bucketAccess))
|
||||
{
|
||||
failed.incrementCountAndAddPrimaryKey(record.getValue("id"));
|
||||
failures.add("no access to the bucket");
|
||||
continue;
|
||||
}
|
||||
try
|
||||
{
|
||||
apply(record, input, output);
|
||||
}
|
||||
catch(StorageException e)
|
||||
{
|
||||
failed.incrementCountAndAddPrimaryKey(record.getValue("id"));
|
||||
if(failures.size() < 5)
|
||||
{
|
||||
failures.add(String.valueOf(record.getValueString("objectKey")) + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
catch(RuntimeException e)
|
||||
{
|
||||
failed.incrementCountAndAddPrimaryKey(record.getValue("id"));
|
||||
if(failures.size() < 5)
|
||||
{
|
||||
failures.add(String.valueOf(record.getValueString("objectKey")) + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Result lines: this action's, then the records that were left alone, then
|
||||
** failures.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public ArrayList<ProcessSummaryLineInterface> getProcessSummary(RunBackendStepOutput output, boolean isForResultScreen)
|
||||
{
|
||||
ArrayList<ProcessSummaryLineInterface> lines = new ArrayList<>();
|
||||
addLines(lines);
|
||||
if(getTransformStep() instanceof BaseTransform transform)
|
||||
{
|
||||
for(ProcessSummaryLine line : transform.skipLines())
|
||||
{
|
||||
line.addSelfToListIfAnyCount(lines);
|
||||
}
|
||||
transform.overCap.addSelfToListIfAnyCount(lines);
|
||||
transform.noAccess.addSelfToListIfAnyCount(lines);
|
||||
}
|
||||
if(failed.getCount() != null && failed.getCount() > 0)
|
||||
{
|
||||
failed.setBulletsOfText(new ArrayList<>(failures));
|
||||
failed.addSelfToListIfAnyCount(lines);
|
||||
}
|
||||
return (lines);
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.processes;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.objectstorage.internal.ObjectStorageRuntime;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.Status;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractTransformStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ProcessSummaryProviderInterface;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The Review (dry run) half of an action. It is read-only: it classifies each
|
||||
** record, produces the summary lines, and passes the records that will be acted
|
||||
** on to the load step. It also applies the bulk cap (10,000 records per run
|
||||
** unless the user has object_storage.admin) and the per-bucket record lock.
|
||||
*******************************************************************************/
|
||||
public abstract class BaseTransform extends AbstractTransformStep implements ProcessSummaryProviderInterface
|
||||
{
|
||||
protected final Map<Integer, Boolean> bucketAccess = new HashMap<>();
|
||||
|
||||
protected final ProcessSummaryLine overCap = EtlSupport.line(Status.ERROR, "record is over the bulk cap and needs object_storage.admin (skipped)",
|
||||
"records are over the bulk cap and need object_storage.admin (skipped)", "record was over the bulk cap (skipped)", "records were over the bulk cap (skipped)");
|
||||
protected final ProcessSummaryLine noAccess = EtlSupport.line(Status.ERROR, "record is in a bucket you may not use (skipped)", "records are in buckets you may not use (skipped)",
|
||||
"record was in a bucket you may not use (skipped)", "records were in buckets you may not use (skipped)");
|
||||
|
||||
private int processed = 0;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The bucket row id a record belongs to (the record's bucketId by default).
|
||||
*******************************************************************************/
|
||||
protected Integer bucketIdOf(QRecord record)
|
||||
{
|
||||
return (record.getValueInteger("bucketId"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Handle one record: classify it, count it, and add it to the output when it
|
||||
** will be acted on.
|
||||
*******************************************************************************/
|
||||
protected abstract void handle(QRecord record, RunBackendStepInput input, RunBackendStepOutput output) throws QException;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Add this action's own summary lines.
|
||||
*******************************************************************************/
|
||||
protected abstract void addLines(ArrayList<ProcessSummaryLineInterface> lines);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The lines about records that will be left alone, as the Result screen shows
|
||||
** them (the load step appends these).
|
||||
*******************************************************************************/
|
||||
protected List<ProcessSummaryLine> skipLines()
|
||||
{
|
||||
return (List.of());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Apply the cap and the bucket lock, then hand each record on.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public final void runOnePage(RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
for(QRecord record : input.getRecords())
|
||||
{
|
||||
processed++;
|
||||
if(processed > ObjectStorageRuntime.getConfig().getBulkCap() && !ActionSupport.isAdmin())
|
||||
{
|
||||
overCap.incrementCountAndAddPrimaryKey(record.getValue("id"));
|
||||
continue;
|
||||
}
|
||||
Integer bucketId = bucketIdOf(record);
|
||||
if(bucketId != null && !ActionSupport.canSeeBucket(bucketId, bucketAccess))
|
||||
{
|
||||
noAccess.incrementCountAndAddPrimaryKey(record.getValue("id"));
|
||||
continue;
|
||||
}
|
||||
handle(record, input, output);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Review lines.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public ArrayList<ProcessSummaryLineInterface> getProcessSummary(RunBackendStepOutput output, boolean isForResultScreen)
|
||||
{
|
||||
ArrayList<ProcessSummaryLineInterface> lines = new ArrayList<>();
|
||||
addLines(lines);
|
||||
overCap.addSelfToListIfAnyCount(lines);
|
||||
noAccess.addSelfToListIfAnyCount(lines);
|
||||
return (lines);
|
||||
}
|
||||
}
|
||||
@@ -1,743 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.processes;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.net.URL;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import com.kingsrook.qbits.objectstorage.api.Capability;
|
||||
import com.kingsrook.qbits.objectstorage.api.ObjectHeaders;
|
||||
import com.kingsrook.qbits.objectstorage.api.PresignGet;
|
||||
import com.kingsrook.qbits.objectstorage.api.PresignPut;
|
||||
import com.kingsrook.qbits.objectstorage.api.StorageKeys;
|
||||
import com.kingsrook.qbits.objectstorage.config.BucketRole;
|
||||
import com.kingsrook.qbits.objectstorage.config.StorageBucketConfig;
|
||||
import com.kingsrook.qbits.objectstorage.index.ObjectIndex;
|
||||
import com.kingsrook.qbits.objectstorage.index.ReferenceScan;
|
||||
import com.kingsrook.qbits.objectstorage.internal.CapabilityStore;
|
||||
import com.kingsrook.qbits.objectstorage.internal.ConfigSync;
|
||||
import com.kingsrook.qbits.objectstorage.internal.ConnectionRuntime;
|
||||
import com.kingsrook.qbits.objectstorage.internal.Db;
|
||||
import com.kingsrook.qbits.objectstorage.internal.ObjectStorageRuntime;
|
||||
import com.kingsrook.qbits.objectstorage.internal.ProtectionRules;
|
||||
import com.kingsrook.qbits.objectstorage.model.ObjectStorageTables;
|
||||
import com.kingsrook.qbits.objectstorage.permissions.ObjectStoragePermissions;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.Status;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import software.amazon.awssdk.services.s3.S3Client;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Bucket record actions: Rescan bucket, Check configuration, Apply header policy
|
||||
** (for a whole bucket), and Create upload link. Bucket creation and deletion
|
||||
** stay in infrastructure-as-code; "Check configuration" only observes, and says
|
||||
** when what it sees differs from what the config declares.
|
||||
*******************************************************************************/
|
||||
public final class BucketProcesses
|
||||
{
|
||||
public static final String RESCAN_NAME = "objectStorageRescanBucket";
|
||||
public static final String CHECK_NAME = "objectStorageCheckConfiguration";
|
||||
public static final String POLICY_NAME = "objectStorageApplyHeaderPolicyToBucket";
|
||||
public static final String UPLOAD_LINK = "objectStorageCreateUploadLink";
|
||||
|
||||
public static final String FIELD_LINK_KEY = "linkKey";
|
||||
public static final String FIELD_LINK_TYPE = "linkContentType";
|
||||
public static final String FIELD_LINK_LENGTH = "linkContentLength";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private BucketProcesses()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The processes.
|
||||
*******************************************************************************/
|
||||
public static List<QProcessMetaData> processes()
|
||||
{
|
||||
return (List.of(
|
||||
EtlSupport.etl(RESCAN_NAME, "Rescan bucket", "refresh", ObjectStorageTables.BUCKET, RescanTransform.class, RescanLoad.class,
|
||||
"This is a preview of the buckets that will be listed and re-indexed.", ObjectStoragePermissions.VIEW),
|
||||
EtlSupport.etl(CHECK_NAME, "Check configuration", "fact_check", ObjectStorageTables.BUCKET, CheckTransform.class, CheckLoad.class,
|
||||
"This is a preview of the buckets whose settings will be read (read-only calls).", ObjectStoragePermissions.VIEW),
|
||||
EtlSupport.etl(POLICY_NAME, "Apply header policy", "rule", ObjectStorageTables.BUCKET, BucketPolicyTransform.class, BucketPolicyLoad.class,
|
||||
"This is a preview of the objects in these buckets whose headers will be rewritten to match the header policy.", ObjectStoragePermissions.WRITE),
|
||||
EtlSupport.withInput(EtlSupport.etl(UPLOAD_LINK, "Create upload link", "upload", ObjectStorageTables.BUCKET, UploadLinkTransform.class, UploadLinkLoad.class,
|
||||
"This is a preview of the presigned upload link that will be created.", ObjectStoragePermissions.WRITE),
|
||||
"Create upload link", "Creates a presigned PUT link for one object key. Anyone with the link can upload that one object until it expires; the content type and length are pinned when given.",
|
||||
EtlSupport.stringField(FIELD_LINK_KEY, "Object key (for example uploads/photo.jpg)", true), EtlSupport.stringField(FIELD_LINK_TYPE, "Content type (pins the upload to this type)", false),
|
||||
new QFieldMetaData(FIELD_LINK_LENGTH, QFieldType.LONG).withLabel("Content length in bytes (pins the upload to this size)"))));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The bucket id of a bucket record is its own id.
|
||||
*******************************************************************************/
|
||||
abstract static class BucketTransform extends BaseTransform
|
||||
{
|
||||
/*******************************************************************************
|
||||
** A bucket record's lock key is its id.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Integer bucketIdOf(QRecord record)
|
||||
{
|
||||
return (record.getValueInteger("id"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The bucket id of a bucket record is its own id.
|
||||
*******************************************************************************/
|
||||
abstract static class BucketLoad extends BaseLoad
|
||||
{
|
||||
/*******************************************************************************
|
||||
** A bucket record's lock key is its id.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Integer bucketIdOf(QRecord record)
|
||||
{
|
||||
return (record.getValueInteger("id"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Rescan bucket, review.
|
||||
*******************************************************************************/
|
||||
public static class RescanTransform extends BucketTransform
|
||||
{
|
||||
private final ProcessSummaryLine will = EtlSupport.line(Status.OK, "bucket will be listed and re-indexed", "buckets will be listed and re-indexed", "bucket was re-indexed", "buckets were re-indexed");
|
||||
private final ProcessSummaryLine notIndexed = EtlSupport.line(Status.INFO, "bucket is not indexed (skipped)", "buckets are not indexed (skipped)", "bucket was not indexed (skipped)", "buckets were not indexed (skipped)");
|
||||
private final ProcessSummaryLine running = EtlSupport.line(Status.WARNING, "bucket already has a scan running (skipped)", "buckets already have scans running (skipped)",
|
||||
"bucket already had a scan running (skipped)", "buckets already had scans running (skipped)");
|
||||
private final List<String> expected = new ArrayList<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Classify one bucket.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void handle(QRecord record, RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
String alias = record.getValueString("alias");
|
||||
StorageBucketConfig config = ActionSupport.config(alias);
|
||||
if(config == null || !Boolean.TRUE.equals(config.getIndexed()))
|
||||
{
|
||||
notIndexed.incrementCountAndAddPrimaryKey(record.getValue("id"));
|
||||
return;
|
||||
}
|
||||
if(ObjectIndex.isScanning(alias))
|
||||
{
|
||||
running.incrementCountAndAddPrimaryKey(record.getValue("id"));
|
||||
return;
|
||||
}
|
||||
will.incrementCountAndAddPrimaryKey(record.getValue("id"));
|
||||
long objects = record.getValueLong("objectCount") == null ? 0 : record.getValueLong("objectCount");
|
||||
expected.add(alias + ": whole bucket; last scan " + (record.getValueInstant("lastScanDate") == null ? "never" : record.getValueInstant("lastScanDate")) + "; about " + (objects / 1000 + 1) + " list calls");
|
||||
output.getRecords().add(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Review lines.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void addLines(ArrayList<ProcessSummaryLineInterface> lines)
|
||||
{
|
||||
will.addSelfToListIfAnyCount(lines);
|
||||
if(will.getCount() != null && will.getCount() > 0)
|
||||
{
|
||||
will.setBulletsOfText(new ArrayList<>(expected));
|
||||
}
|
||||
notIndexed.addSelfToListIfAnyCount(lines);
|
||||
running.addSelfToListIfAnyCount(lines);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The skips, as the Result screen shows them.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected List<ProcessSummaryLine> skipLines()
|
||||
{
|
||||
return (List.of(notIndexed, running));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Rescan bucket, execute: scan, HEAD backlog, references.
|
||||
*******************************************************************************/
|
||||
public static class RescanLoad extends BucketLoad
|
||||
{
|
||||
private final ArrayList<ProcessSummaryLineInterface> results = new ArrayList<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Scan one bucket.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void apply(QRecord record, RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
String alias = record.getValueString("alias");
|
||||
ObjectIndex.ScanResult scan = ObjectIndex.scan(alias);
|
||||
boolean ok = "OK".equals(scan.status());
|
||||
results.add(new ProcessSummaryLine(ok ? Status.OK : Status.WARNING, alias + ": scan " + scan.status() + " - " + scan.seen() + " objects seen, " + scan.added() + " new, "
|
||||
+ scan.updated() + " updated, " + scan.removed() + " removed" + (scan.markedDeleted() > 0 ? ", " + scan.markedDeleted() + " marked deleted" : "")
|
||||
+ (scan.note() == null ? "" : ". " + scan.note())));
|
||||
if(ok)
|
||||
{
|
||||
int headed = ObjectIndex.headBacklog(alias, ObjectStorageRuntime.getConfig().getHeadBacklogPerRun());
|
||||
results.add(EtlSupport.text(Status.INFO, alias + ": headers read for " + headed + " objects"));
|
||||
if(ReferenceScan.applies(ActionSupport.config(alias)))
|
||||
{
|
||||
ReferenceScan.Result r = ReferenceScan.run(alias);
|
||||
results.add(new ProcessSummaryLine(r.missing() > 0 ? Status.WARNING : Status.INFO, alias + ": " + r.references() + " references, " + r.missing() + " missing, " + r.orphaned() + " orphaned objects"));
|
||||
}
|
||||
}
|
||||
output.getRecords().add(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Result lines.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void addLines(ArrayList<ProcessSummaryLineInterface> lines)
|
||||
{
|
||||
lines.addAll(results);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Check configuration, review.
|
||||
*******************************************************************************/
|
||||
public static class CheckTransform extends BucketTransform
|
||||
{
|
||||
private final ProcessSummaryLine will = EtlSupport.line(Status.OK, "bucket's versioning, lifecycle, policy and CORS will be read", "buckets' versioning, lifecycle, policy and CORS will be read",
|
||||
"bucket's settings were read", "buckets' settings were read");
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Count.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void handle(QRecord record, RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
will.incrementCountAndAddPrimaryKey(record.getValue("id"));
|
||||
output.getRecords().add(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Review lines.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void addLines(ArrayList<ProcessSummaryLineInterface> lines)
|
||||
{
|
||||
will.addSelfToListIfAnyCount(lines);
|
||||
lines.add(EtlSupport.text(Status.INFO, "Buckets are created and configured in infrastructure-as-code. This only observes; it changes nothing."));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** What the provider says a bucket is set to.
|
||||
*******************************************************************************/
|
||||
public record Observed(String versioning, String lifecycle, String policy, String cors, boolean versioningEnabled, boolean hasLifecycle, boolean hasPolicy, boolean hasCors)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Read a bucket's settings (read-only calls). Every call is isolated: a
|
||||
** provider that does not implement one reports "not available".
|
||||
*******************************************************************************/
|
||||
public static Observed observe(StorageBucketConfig bucket)
|
||||
{
|
||||
ConnectionRuntime connection = ObjectStorageRuntime.connection(bucket.getConnection());
|
||||
S3Client client = connection.client();
|
||||
String name = com.kingsrook.qbits.objectstorage.internal.SecretRefs.resolveSetting(bucket.getBucketName());
|
||||
|
||||
String versioning = "not available";
|
||||
boolean versioningEnabled = false;
|
||||
try
|
||||
{
|
||||
String status = client.getBucketVersioning(b -> b.bucket(name)).statusAsString();
|
||||
versioning = status == null ? "Off" : status;
|
||||
versioningEnabled = "Enabled".equals(status);
|
||||
}
|
||||
catch(RuntimeException e)
|
||||
{
|
||||
versioning = "not available (" + shortError(e) + ")";
|
||||
}
|
||||
|
||||
String lifecycle = "not available";
|
||||
boolean hasLifecycle = false;
|
||||
try
|
||||
{
|
||||
var rules = client.getBucketLifecycleConfiguration(b -> b.bucket(name)).rules();
|
||||
hasLifecycle = !rules.isEmpty();
|
||||
List<String> parts = new ArrayList<>();
|
||||
rules.forEach(r -> parts.add((r.id() == null ? "rule" : r.id()) + (r.expiration() != null && r.expiration().days() != null ? " expires after " + r.expiration().days() + " days" : "")
|
||||
+ (r.noncurrentVersionExpiration() != null && r.noncurrentVersionExpiration().noncurrentDays() != null ? ", noncurrent versions after " + r.noncurrentVersionExpiration().noncurrentDays() + " days" : "")));
|
||||
lifecycle = rules.isEmpty() ? "none" : rules.size() + " rule(s): " + String.join("; ", parts);
|
||||
}
|
||||
catch(RuntimeException e)
|
||||
{
|
||||
lifecycle = isNotConfigured(e, "NoSuchLifecycleConfiguration") ? "none" : "not available (" + shortError(e) + ")";
|
||||
}
|
||||
|
||||
String policy = "not available";
|
||||
boolean hasPolicy = false;
|
||||
try
|
||||
{
|
||||
String json = client.getBucketPolicy(b -> b.bucket(name)).policy();
|
||||
int statements = json == null ? 0 : json.split("\"Effect\"", -1).length - 1;
|
||||
hasPolicy = statements > 0;
|
||||
policy = statements + " statement(s)";
|
||||
}
|
||||
catch(RuntimeException e)
|
||||
{
|
||||
policy = isNotConfigured(e, "NoSuchBucketPolicy") ? "none" : "not available (" + shortError(e) + ")";
|
||||
}
|
||||
|
||||
String cors = "not available";
|
||||
boolean hasCors = false;
|
||||
try
|
||||
{
|
||||
int rules = client.getBucketCors(b -> b.bucket(name)).corsRules().size();
|
||||
hasCors = rules > 0;
|
||||
cors = rules == 0 ? "none" : rules + " rule(s)";
|
||||
}
|
||||
catch(RuntimeException e)
|
||||
{
|
||||
cors = isNotConfigured(e, "NoSuchCORSConfiguration") ? "none" : "not available (" + shortError(e) + ")";
|
||||
}
|
||||
return (new Observed(versioning, lifecycle, policy, cors, versioningEnabled, hasLifecycle, hasPolicy, hasCors));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The differences between what the config declares and what is observed.
|
||||
*******************************************************************************/
|
||||
public static List<String> drift(StorageBucketConfig bucket, Observed o)
|
||||
{
|
||||
List<String> drift = new ArrayList<>();
|
||||
if(bucket.getExpectVersioning() != null && bucket.getExpectVersioning() != o.versioningEnabled())
|
||||
{
|
||||
drift.add("expected versioning " + (bucket.getExpectVersioning() ? "enabled" : "off") + ", observed " + o.versioning());
|
||||
}
|
||||
if(bucket.getExpectLifecycle() != null && bucket.getExpectLifecycle() != o.hasLifecycle())
|
||||
{
|
||||
drift.add("expected " + (bucket.getExpectLifecycle() ? "a lifecycle configuration" : "no lifecycle configuration") + ", observed " + o.lifecycle());
|
||||
}
|
||||
if(bucket.getExpectCors() != null && bucket.getExpectCors() != o.hasCors())
|
||||
{
|
||||
drift.add("expected " + (bucket.getExpectCors() ? "CORS rules" : "no CORS rules") + ", observed " + o.cors());
|
||||
}
|
||||
if(bucket.getExpectPolicy() != null && bucket.getExpectPolicy() != o.hasPolicy())
|
||||
{
|
||||
drift.add("expected " + (bucket.getExpectPolicy() ? "a bucket policy" : "no bucket policy") + ", observed " + o.policy());
|
||||
}
|
||||
return (drift);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether an exception is the documented "not configured" answer.
|
||||
*******************************************************************************/
|
||||
private static boolean isNotConfigured(RuntimeException e, String code)
|
||||
{
|
||||
return (code.equals(com.kingsrook.qbits.objectstorage.internal.ErrorMapper.errorCode(e)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A short, safe description of an error.
|
||||
*******************************************************************************/
|
||||
private static String shortError(RuntimeException e)
|
||||
{
|
||||
String code = com.kingsrook.qbits.objectstorage.internal.ErrorMapper.errorCode(e);
|
||||
return (code != null ? code : e.getClass().getSimpleName());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Check configuration, execute.
|
||||
*******************************************************************************/
|
||||
public static class CheckLoad extends BucketLoad
|
||||
{
|
||||
private final ArrayList<ProcessSummaryLineInterface> results = new ArrayList<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Observe one bucket and record what was seen.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void apply(QRecord record, RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
String alias = record.getValueString("alias");
|
||||
StorageBucketConfig bucket = ActionSupport.config(alias);
|
||||
Observed o = observe(bucket);
|
||||
List<String> drift = drift(bucket, o);
|
||||
Db.update(ObjectStorageTables.BUCKET, List.of(new QRecord().withValue("id", record.getValue("id")).withValue("versioningState", o.versioning().length() > 30 ? o.versioning().substring(0, 30) : o.versioning())
|
||||
.withValue("lifecycleSummary", o.lifecycle()).withValue("policySummary", o.policy()).withValue("corsSummary", o.cors())
|
||||
.withValue("configDriftNote", drift.isEmpty() ? null : String.join("; ", drift))));
|
||||
results.add(EtlSupport.text(Status.INFO, alias + ": versioning " + o.versioning() + ", lifecycle " + o.lifecycle() + ", policy " + o.policy() + ", CORS " + o.cors()
|
||||
+ " (managed by " + bucket.getManagedBy() + ")"));
|
||||
for(String d : drift)
|
||||
{
|
||||
results.add(EtlSupport.text(Status.WARNING, alias + ": drift - " + d));
|
||||
}
|
||||
output.getRecords().add(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Result lines.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void addLines(ArrayList<ProcessSummaryLineInterface> lines)
|
||||
{
|
||||
lines.addAll(results);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Apply header policy to a bucket, review: counts from the index.
|
||||
*******************************************************************************/
|
||||
public static class BucketPolicyTransform extends BucketTransform
|
||||
{
|
||||
private final ProcessSummaryLine change = EtlSupport.line(Status.OK, "object in this bucket will change to match the policy", "objects in these buckets will change to match the policy",
|
||||
"object was changed to match the policy", "objects were changed to match the policy");
|
||||
private final ProcessSummaryLine unknown = EtlSupport.line(Status.WARNING, "object has unknown headers (not yet read; refresh it or wait for the nightly HEAD backlog)",
|
||||
"objects have unknown headers (not yet read; refresh them or wait for the nightly HEAD backlog)", "object had unknown headers", "objects had unknown headers");
|
||||
private final ProcessSummaryLine backup = EtlSupport.line(Status.INFO, "bucket is a backup bucket (the policy never applies there)", "buckets are backup buckets (the policy never applies there)",
|
||||
"bucket was a backup bucket (skipped)", "buckets were backup buckets (skipped)");
|
||||
private final ProcessSummaryLine notIndexed = EtlSupport.line(Status.INFO, "bucket is not indexed, so its objects cannot be listed here (skipped)", "buckets are not indexed, so their objects cannot be listed here (skipped)",
|
||||
"bucket was not indexed (skipped)", "buckets were not indexed (skipped)");
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Count what would change.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void handle(QRecord record, RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
StorageBucketConfig config = ActionSupport.config(record.getValueString("alias"));
|
||||
if(config == null || config.getRole() == BucketRole.BACKUP)
|
||||
{
|
||||
backup.incrementCountAndAddPrimaryKey(record.getValue("id"));
|
||||
return;
|
||||
}
|
||||
if(!Boolean.TRUE.equals(config.getIndexed()))
|
||||
{
|
||||
notIndexed.incrementCountAndAddPrimaryKey(record.getValue("id"));
|
||||
return;
|
||||
}
|
||||
Integer id = record.getValueInteger("id");
|
||||
int mismatched = Db.count(ObjectStorageTables.OBJECT, new QQueryFilter(Db.eq("bucketId", id), Db.eq("deleted", false), Db.eq("headersKnown", true), Db.eq("matchesHeaderPolicy", false)));
|
||||
int unknownCount = Db.count(ObjectStorageTables.OBJECT, new QQueryFilter(Db.eq("bucketId", id), Db.eq("deleted", false), Db.eq("headersKnown", false)));
|
||||
change.incrementCount(mismatched);
|
||||
unknown.incrementCount(unknownCount);
|
||||
if(mismatched > ObjectStorageRuntime.getConfig().getBulkCap() && !ActionSupport.isAdmin())
|
||||
{
|
||||
overCap.incrementCount(mismatched - ObjectStorageRuntime.getConfig().getBulkCap());
|
||||
}
|
||||
output.getRecords().add(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Review lines.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void addLines(ArrayList<ProcessSummaryLineInterface> lines)
|
||||
{
|
||||
change.addSelfToListIfAnyCount(lines);
|
||||
unknown.addSelfToListIfAnyCount(lines);
|
||||
backup.addSelfToListIfAnyCount(lines);
|
||||
notIndexed.addSelfToListIfAnyCount(lines);
|
||||
if(change.getCount() != null && change.getCount() > 0)
|
||||
{
|
||||
lines.add(EtlSupport.text(Status.INFO, "Rewriting headers copies each object onto itself, which changes Last-Modified and can change the ETag."));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The skips, as the Result screen shows them.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected List<ProcessSummaryLine> skipLines()
|
||||
{
|
||||
return (List.of(unknown, backup, notIndexed));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Apply header policy to a bucket, execute: the objects the index says are off.
|
||||
*******************************************************************************/
|
||||
public static class BucketPolicyLoad extends BucketLoad
|
||||
{
|
||||
private final ProcessSummaryLine changed = EtlSupport.line(Status.OK, "object will be changed to match the policy", "objects will be changed to match the policy",
|
||||
"object was changed to match the policy", "objects were changed to match the policy");
|
||||
private final ProcessSummaryLine failedObjects = EtlSupport.line(Status.ERROR, "object could not be changed (see Operations)", "objects could not be changed (see Operations)",
|
||||
"object could not be changed (see Operations)", "objects could not be changed (see Operations)");
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Rewrite the headers of one bucket's mismatching objects (up to the bulk cap
|
||||
** unless admin).
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void apply(QRecord record, RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
String alias = record.getValueString("alias");
|
||||
StorageBucketConfig config = ActionSupport.config(alias);
|
||||
int limit = ActionSupport.isAdmin() ? Integer.MAX_VALUE : ObjectStorageRuntime.getConfig().getBulkCap();
|
||||
List<QRecord> todo = new ArrayList<>();
|
||||
Db.forEachPage(ObjectStorageTables.OBJECT, new QQueryFilter(Db.eq("bucketId", record.getValue("id")), Db.eq("deleted", false), Db.eq("headersKnown", true), Db.eq("matchesHeaderPolicy", false)), 2000,
|
||||
rows -> rows.forEach(r ->
|
||||
{
|
||||
if(todo.size() < limit)
|
||||
{
|
||||
todo.add(r);
|
||||
}
|
||||
}));
|
||||
for(QRecord row : todo)
|
||||
{
|
||||
String key = row.getValueString("objectKey");
|
||||
if(ProtectionRules.check(alias, key, ProtectionRules.Op.WRITE, com.kingsrook.qbits.objectstorage.api.StorageCaller.user()).blocked())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
try
|
||||
{
|
||||
ObjectHeaders wanted = ObjectStorageRuntime.headerPolicy().headersFor(config, key, row.getValueString("contentType"));
|
||||
ActionSupport.storage(alias).updateHeaders(key, new ObjectHeaders().withCacheControl(wanted.getCacheControl()).withContentDisposition(wanted.getContentDisposition()));
|
||||
changed.incrementCount();
|
||||
}
|
||||
catch(RuntimeException e)
|
||||
{
|
||||
failedObjects.incrementCount();
|
||||
}
|
||||
}
|
||||
output.getRecords().add(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Result lines.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void addLines(ArrayList<ProcessSummaryLineInterface> lines)
|
||||
{
|
||||
changed.addSelfToListIfAnyCount(lines);
|
||||
failedObjects.addSelfToListIfAnyCount(lines);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Create upload link, review: the key must be valid; the Result shows the link once.
|
||||
*******************************************************************************/
|
||||
public static class UploadLinkTransform extends BucketTransform
|
||||
{
|
||||
private final ProcessSummaryLine will = EtlSupport.line(Status.OK, "upload link will be created", "upload links will be created", "upload link was created", "upload links were created");
|
||||
private String keyError;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Validate the key and the TTL cap.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void handle(QRecord record, RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
try
|
||||
{
|
||||
StorageKeys.normalize(input.getValueString(FIELD_LINK_KEY));
|
||||
}
|
||||
catch(IllegalArgumentException e)
|
||||
{
|
||||
keyError = e.getMessage();
|
||||
return;
|
||||
}
|
||||
StorageBucketConfig config = ActionSupport.config(record.getValueString("alias"));
|
||||
if(config == null || config.getRole() == BucketRole.BACKUP)
|
||||
{
|
||||
keyError = "upload links are not offered for backup buckets";
|
||||
return;
|
||||
}
|
||||
will.incrementCountAndAddPrimaryKey(record.getValue("id"));
|
||||
output.getRecords().add(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Review lines.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void addLines(ArrayList<ProcessSummaryLineInterface> lines)
|
||||
{
|
||||
if(keyError != null)
|
||||
{
|
||||
lines.add(EtlSupport.text(Status.ERROR, "The link cannot be created: " + keyError));
|
||||
return;
|
||||
}
|
||||
will.addSelfToListIfAnyCount(lines);
|
||||
lines.add(EtlSupport.text(Status.INFO, "A presigned PUT cannot limit how large the upload is, only pin one exact size and type. The link is a bearer token: share it only with the intended uploader."));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Create upload link, execute.
|
||||
*******************************************************************************/
|
||||
public static class UploadLinkLoad extends BucketLoad
|
||||
{
|
||||
private final ArrayList<ProcessSummaryLineInterface> results = new ArrayList<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Create the link and show it once.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void apply(QRecord record, RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
String alias = record.getValueString("alias");
|
||||
PresignPut request = PresignPut.of(StorageKeys.normalize(input.getValueString(FIELD_LINK_KEY)));
|
||||
if(StringUtils.hasContent(input.getValueString(FIELD_LINK_TYPE)))
|
||||
{
|
||||
request.setContentType(input.getValueString(FIELD_LINK_TYPE));
|
||||
}
|
||||
if(input.getValue(FIELD_LINK_LENGTH) != null && StringUtils.hasContent(String.valueOf(input.getValue(FIELD_LINK_LENGTH))))
|
||||
{
|
||||
request.setContentLength(Long.valueOf(String.valueOf(input.getValue(FIELD_LINK_LENGTH))));
|
||||
}
|
||||
URL url = ActionSupport.storage(alias).presignPut(request);
|
||||
int minutes = (int) Math.min(ConfigSync.effectiveMaxPresignPutSeconds(ActionSupport.config(alias)) / 60, 15);
|
||||
ProcessSummaryLine line = EtlSupport.text(Status.OK, "Upload link for " + alias + "/" + request.getKey() + " (expires in about " + minutes + " minutes; shown once)");
|
||||
line.setBulletsOfText(new ArrayList<>(List.of(url.toString())));
|
||||
results.add(line);
|
||||
output.getRecords().add(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Result lines.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void addLines(ArrayList<ProcessSummaryLineInterface> lines)
|
||||
{
|
||||
lines.addAll(results);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The capability names a link needs (documented in the Review).
|
||||
*******************************************************************************/
|
||||
static boolean linksSupported(String alias)
|
||||
{
|
||||
return (!CapabilityStore.ruledOut(ActionSupport.storage(alias).capability(Capability.PRESIGNED_PUT)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A readable TTL.
|
||||
*******************************************************************************/
|
||||
static String describe(Duration ttl)
|
||||
{
|
||||
return (String.format(Locale.ROOT, "%d minutes", ttl.toMinutes()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A bulk-edit friendly id list of a page.
|
||||
*******************************************************************************/
|
||||
static List<Serializable> idsOf(List<QRecord> records)
|
||||
{
|
||||
List<Serializable> ids = new ArrayList<>();
|
||||
records.forEach(r -> ids.add(r.getValue("id")));
|
||||
return (ids);
|
||||
}
|
||||
}
|
||||
@@ -1,521 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.processes;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.objectstorage.api.CopyRequest;
|
||||
import com.kingsrook.qbits.objectstorage.api.DeleteRequest;
|
||||
import com.kingsrook.qbits.objectstorage.api.StorageCaller;
|
||||
import com.kingsrook.qbits.objectstorage.api.StorageKeys;
|
||||
import com.kingsrook.qbits.objectstorage.internal.Db;
|
||||
import com.kingsrook.qbits.objectstorage.internal.ProtectionRules;
|
||||
import com.kingsrook.qbits.objectstorage.model.ObjectStorageTables;
|
||||
import com.kingsrook.qbits.objectstorage.permissions.ObjectStoragePermissions;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.Status;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** "Copy objects" and "Move or rename objects" (also the engine of the folder
|
||||
** rename). A move is a copy followed by a delete, not an atomic operation, and
|
||||
** the Review says so. Moves skip referenced, protected and locked objects;
|
||||
** copies never overwrite unless asked.
|
||||
*******************************************************************************/
|
||||
public final class CopyMoveProcesses
|
||||
{
|
||||
public static final String COPY_NAME = "objectStorageCopyObjects";
|
||||
public static final String MOVE_NAME = "objectStorageMoveObjects";
|
||||
|
||||
public static final String FIELD_DEST_BUCKET = "destinationBucket";
|
||||
public static final String FIELD_DEST_PREFIX = "destinationPrefix";
|
||||
public static final String FIELD_KEEP_PATH = "keepPath";
|
||||
public static final String FIELD_OVERWRITE = "overwrite";
|
||||
public static final String FIELD_NEW_NAME = "newName";
|
||||
|
||||
private static final long LARGE_OBJECT = 5L * 1024 * 1024 * 1024;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private CopyMoveProcesses()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The processes.
|
||||
*******************************************************************************/
|
||||
public static List<QProcessMetaData> processes()
|
||||
{
|
||||
QFieldMetaData bucket = new QFieldMetaData(FIELD_DEST_BUCKET, QFieldType.INTEGER).withLabel("Destination bucket (leave blank for the same bucket)")
|
||||
.withPossibleValueSourceName(ObjectStorageTables.BUCKET);
|
||||
QFieldMetaData prefix = EtlSupport.stringField(FIELD_DEST_PREFIX, "Destination folder (for example media/2026/; leave blank for the bucket root)", false);
|
||||
QFieldMetaData keep = EtlSupport.booleanField(FIELD_KEEP_PATH, "Keep each object's whole path under the destination (otherwise only its file name)", false);
|
||||
QFieldMetaData over = EtlSupport.booleanField(FIELD_OVERWRITE, "Overwrite objects that already exist at the destination", false);
|
||||
QFieldMetaData name = EtlSupport.stringField(FIELD_NEW_NAME, "New file name (only when moving one object)", false);
|
||||
|
||||
return (List.of(
|
||||
EtlSupport.withInput(EtlSupport.etl(COPY_NAME, "Copy objects", "content_copy", ObjectStorageTables.OBJECT, CopyTransform.class, CopyLoad.class,
|
||||
"This is a preview of the objects that will be copied.", ObjectStoragePermissions.WRITE),
|
||||
"Copy objects", "Copies the selected objects to a bucket and folder. The originals stay where they are.", bucket, prefix, keep, over),
|
||||
EtlSupport.withInput(EtlSupport.etl(MOVE_NAME, "Move or rename objects", "drive_file_move", ObjectStorageTables.OBJECT, MoveTransform.class, MoveLoad.class,
|
||||
"This is a preview of the objects that will be moved.", ObjectStoragePermissions.WRITE),
|
||||
"Move or rename objects", "Moves the selected objects to a bucket and folder, or renames a single object. A move is a copy followed by a delete, not one atomic step.",
|
||||
bucket, prefix, keep, over, name)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Where an object goes: the destination folder plus the file name or the whole
|
||||
** path; a single-object rename replaces the file name.
|
||||
*******************************************************************************/
|
||||
static String destinationKey(String sourceKey, String destPrefix, boolean keepPath, String newName)
|
||||
{
|
||||
String name = sourceKey.substring(sourceKey.lastIndexOf('/') + 1);
|
||||
String folder = sourceKey.contains("/") ? sourceKey.substring(0, sourceKey.lastIndexOf('/') + 1) : "";
|
||||
String prefix = destPrefix == null ? "" : destPrefix.trim();
|
||||
if(!prefix.isEmpty() && !prefix.endsWith("/"))
|
||||
{
|
||||
prefix = prefix + "/";
|
||||
}
|
||||
boolean rename = StringUtils.hasContent(newName);
|
||||
if(rename && prefix.isEmpty())
|
||||
{
|
||||
// a rename with no destination folder stays in the object's own folder
|
||||
return (StorageKeys.normalize(folder + newName.trim()));
|
||||
}
|
||||
String leaf = rename ? newName.trim() : name;
|
||||
return (StorageKeys.normalize(prefix + (keepPath ? folder : "") + leaf));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The alias of a bucket row id (the destination bucket field), or null.
|
||||
*******************************************************************************/
|
||||
static String aliasOfBucketId(Serializable bucketId)
|
||||
{
|
||||
if(bucketId == null || !StringUtils.hasContent(String.valueOf(bucketId)))
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
try
|
||||
{
|
||||
QRecord bucket = Db.asSystem(() -> Db.first(ObjectStorageTables.BUCKET, "id", Integer.valueOf(String.valueOf(bucketId))));
|
||||
return (bucket == null ? null : bucket.getValueString("alias"));
|
||||
}
|
||||
catch(QException | NumberFormatException e)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The shared review of copy and move.
|
||||
*******************************************************************************/
|
||||
public abstract static class CopyMoveTransform extends BaseTransform
|
||||
{
|
||||
protected final ProcessSummaryLine will;
|
||||
protected final ProcessSummaryLine overwrites = EtlSupport.line(Status.WARNING, "object will replace one that already exists at the destination", "objects will replace ones that already exist at the destination",
|
||||
"object replaced one that already existed at the destination", "objects replaced ones that already existed at the destination");
|
||||
protected final ProcessSummaryLine exists = EtlSupport.line(Status.WARNING, "object is skipped: the destination already exists (and overwrite is off)",
|
||||
"objects are skipped: the destinations already exist (and overwrite is off)", "object was skipped: the destination already existed", "objects were skipped: the destinations already existed");
|
||||
protected final ProcessSummaryLine same = EtlSupport.line(Status.INFO, "object is skipped: it is already at the destination", "objects are skipped: they are already at the destination",
|
||||
"object was skipped: it is already at the destination", "objects were skipped: they are already at the destination");
|
||||
protected final ProcessSummaryLine invalid = EtlSupport.line(Status.ERROR, "object is skipped: its destination key is not valid", "objects are skipped: their destination keys are not valid",
|
||||
"object was skipped: its destination key was not valid", "objects were skipped: their destination keys were not valid");
|
||||
protected final ProcessSummaryLine referenced = EtlSupport.line(Status.WARNING, "object is skipped: it is referenced", "objects are skipped: they are referenced",
|
||||
"object was skipped: it is referenced", "objects were skipped: they are referenced");
|
||||
protected final ProcessSummaryLine protectedLine = EtlSupport.line(Status.WARNING, "object is skipped: it (or its destination) is protected", "objects are skipped: they (or their destinations) are protected",
|
||||
"object was skipped: it (or its destination) is protected", "objects were skipped: they (or their destinations) are protected");
|
||||
protected final ProcessSummaryLine locked = EtlSupport.line(Status.WARNING, "object is skipped: it is under object lock", "objects are skipped: they are under object lock",
|
||||
"object was skipped: it is under object lock", "objects were skipped: they are under object lock");
|
||||
protected final ProcessSummaryLine backup = EtlSupport.line(Status.INFO, "object is skipped: it is in a backup bucket", "objects are skipped: they are in backup buckets",
|
||||
"object was skipped: it is in a backup bucket", "objects were skipped: they are in backup buckets");
|
||||
protected final ProcessSummaryLine deletedRows = EtlSupport.line(Status.INFO, "object is skipped: it is deleted", "objects are skipped: they are deleted", "object was skipped: it is deleted",
|
||||
"objects were skipped: they are deleted");
|
||||
protected final ProcessSummaryLine large = EtlSupport.line(Status.INFO, "object is over 5 GB and is copied in parts", "objects are over 5 GB and are copied in parts",
|
||||
"object was over 5 GB and was copied in parts", "objects were over 5 GB and were copied in parts");
|
||||
|
||||
private long totalBytes = 0;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor.
|
||||
*******************************************************************************/
|
||||
protected CopyMoveTransform(ProcessSummaryLine will)
|
||||
{
|
||||
this.will = will;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether this is a move (the source is deleted afterwards).
|
||||
*******************************************************************************/
|
||||
protected abstract boolean isMove();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Where the object goes, for this run's inputs (the folder rename overrides).
|
||||
*******************************************************************************/
|
||||
protected String destKeyFor(QRecord record, RunBackendStepInput input)
|
||||
{
|
||||
Integer total = input.getValueInteger("recordCount");
|
||||
String newName = isMove() && total != null && total == 1 ? input.getValueString(FIELD_NEW_NAME) : null;
|
||||
return (destinationKey(record.getValueString("objectKey"), input.getValueString(FIELD_DEST_PREFIX), Boolean.TRUE.equals(input.getValueBoolean(FIELD_KEEP_PATH)), newName));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Classify one object.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void handle(QRecord record, RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
Serializable id = record.getValue("id");
|
||||
if(Boolean.TRUE.equals(record.getValueBoolean("deleted")))
|
||||
{
|
||||
deletedRows.incrementCountAndAddPrimaryKey(id);
|
||||
return;
|
||||
}
|
||||
ActionSupport.Classification c = ActionSupport.classify(record, isMove() ? ProtectionRules.Op.DELETE : ProtectionRules.Op.WRITE, isMove(), isMove());
|
||||
switch(c.skip())
|
||||
{
|
||||
case REFERENCED:
|
||||
referenced.incrementCountAndAddPrimaryKey(id);
|
||||
return;
|
||||
case PROTECTED:
|
||||
protectedLine.incrementCountAndAddPrimaryKey(id);
|
||||
return;
|
||||
case LOCKED:
|
||||
locked.incrementCountAndAddPrimaryKey(id);
|
||||
return;
|
||||
case BACKUP_BUCKET:
|
||||
backup.incrementCountAndAddPrimaryKey(id);
|
||||
return;
|
||||
case NO_BUCKET_ACCESS:
|
||||
noAccess.incrementCountAndAddPrimaryKey(id);
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
String srcAlias = ActionSupport.aliasOf(record);
|
||||
String destAlias = aliasOfBucketId(input.getValue(FIELD_DEST_BUCKET));
|
||||
destAlias = destAlias == null ? srcAlias : destAlias;
|
||||
if(ActionSupport.config(destAlias) != null && !ActionSupport.canSeeBucket(com.kingsrook.qbits.objectstorage.internal.ConfigSync.bucketId(destAlias), bucketAccess))
|
||||
{
|
||||
noAccess.incrementCountAndAddPrimaryKey(id);
|
||||
return;
|
||||
}
|
||||
String destKey;
|
||||
try
|
||||
{
|
||||
destKey = destKeyFor(record, input);
|
||||
}
|
||||
catch(IllegalArgumentException e)
|
||||
{
|
||||
invalid.incrementCountAndAddPrimaryKey(id);
|
||||
return;
|
||||
}
|
||||
if(destAlias.equals(srcAlias) && destKey.equals(record.getValueString("objectKey")))
|
||||
{
|
||||
same.incrementCountAndAddPrimaryKey(id);
|
||||
return;
|
||||
}
|
||||
if(ActionSupport.config(destAlias) == null || ActionSupport.config(destAlias).getRole() == com.kingsrook.qbits.objectstorage.config.BucketRole.BACKUP
|
||||
|| ProtectionRules.check(destAlias, destKey, ProtectionRules.Op.WRITE, StorageCaller.user()).blocked())
|
||||
{
|
||||
protectedLine.incrementCountAndAddPrimaryKey(id);
|
||||
return;
|
||||
}
|
||||
boolean destExists = false;
|
||||
try
|
||||
{
|
||||
destExists = ActionSupport.storage(destAlias).head(destKey).isPresent();
|
||||
}
|
||||
catch(RuntimeException e)
|
||||
{
|
||||
destExists = false;
|
||||
}
|
||||
if(destExists)
|
||||
{
|
||||
if(!Boolean.TRUE.equals(input.getValueBoolean(FIELD_OVERWRITE)))
|
||||
{
|
||||
exists.incrementCountAndAddPrimaryKey(id);
|
||||
return;
|
||||
}
|
||||
overwrites.incrementCountAndAddPrimaryKey(id);
|
||||
}
|
||||
Long size = record.getValueLong("size");
|
||||
totalBytes += size == null ? 0 : size;
|
||||
if(size != null && size > LARGE_OBJECT)
|
||||
{
|
||||
large.incrementCountAndAddPrimaryKey(id);
|
||||
}
|
||||
will.incrementCountAndAddPrimaryKey(id);
|
||||
record.setValue("_destAlias", destAlias);
|
||||
record.setValue("_destKey", destKey);
|
||||
output.getRecords().add(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Review lines.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void addLines(ArrayList<ProcessSummaryLineInterface> lines)
|
||||
{
|
||||
will.addSelfToListIfAnyCount(lines);
|
||||
if(will.getCount() != null && will.getCount() > 0)
|
||||
{
|
||||
lines.add(EtlSupport.text(Status.INFO, ActionSupport.bytes(totalBytes) + " in total"));
|
||||
if(isMove())
|
||||
{
|
||||
lines.add(EtlSupport.text(Status.INFO, "A move is a copy followed by a delete, not one atomic step. If the delete fails, the object exists in both places."));
|
||||
}
|
||||
}
|
||||
overwrites.addSelfToListIfAnyCount(lines);
|
||||
large.addSelfToListIfAnyCount(lines);
|
||||
for(ProcessSummaryLine line : skipLines())
|
||||
{
|
||||
line.addSelfToListIfAnyCount(lines);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The skips, as the Result screen shows them.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected List<ProcessSummaryLine> skipLines()
|
||||
{
|
||||
return (List.of(exists, same, invalid, referenced, protectedLine, locked, backup, deletedRows));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Copy, review.
|
||||
*******************************************************************************/
|
||||
public static class CopyTransform extends CopyMoveTransform
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Constructor.
|
||||
*******************************************************************************/
|
||||
public CopyTransform()
|
||||
{
|
||||
super(EtlSupport.line(Status.OK, "object will be copied", "objects will be copied", "object was copied", "objects were copied"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Not a move.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected boolean isMove()
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Move, review.
|
||||
*******************************************************************************/
|
||||
public static class MoveTransform extends CopyMoveTransform
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Constructor.
|
||||
*******************************************************************************/
|
||||
public MoveTransform()
|
||||
{
|
||||
super(EtlSupport.line(Status.OK, "object will be moved", "objects will be moved", "object was moved", "objects were moved"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A move.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected boolean isMove()
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Copy and move, execute.
|
||||
*******************************************************************************/
|
||||
public abstract static class CopyMoveLoad extends BaseLoad
|
||||
{
|
||||
private final ProcessSummaryLine done;
|
||||
private final ProcessSummaryLine copiedNotDeleted = EtlSupport.line(Status.ERROR, "object was copied but the original could not be deleted (it exists in both places)",
|
||||
"objects were copied but the originals could not be deleted (they exist in both places)", "object was copied but the original could not be deleted (it exists in both places)",
|
||||
"objects were copied but the originals could not be deleted (they exist in both places)");
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor.
|
||||
*******************************************************************************/
|
||||
protected CopyMoveLoad(ProcessSummaryLine done)
|
||||
{
|
||||
this.done = done;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether this is a move.
|
||||
*******************************************************************************/
|
||||
protected abstract boolean isMove();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Copy one object, and for a move delete the original.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void apply(QRecord record, RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
String srcAlias = ActionSupport.aliasOf(record);
|
||||
String srcKey = record.getValueString("objectKey");
|
||||
String destAlias = record.getValueString("_destAlias");
|
||||
String destKey = record.getValueString("_destKey");
|
||||
if(isMove() && !ActionSupport.has(ObjectStoragePermissions.DELETE))
|
||||
{
|
||||
throw (new com.kingsrook.qbits.objectstorage.api.StorageException(com.kingsrook.qbits.objectstorage.api.StorageException.Reason.ACCESS_DENIED, "a move needs object_storage.delete too"));
|
||||
}
|
||||
ActionSupport.storage(srcAlias).copy(CopyRequest.of(srcKey, destKey).withDestinationBucket(destAlias.equals(srcAlias) ? null : destAlias));
|
||||
if(isMove())
|
||||
{
|
||||
try
|
||||
{
|
||||
ActionSupport.storage(srcAlias).delete(DeleteRequest.of(srcKey));
|
||||
}
|
||||
catch(RuntimeException e)
|
||||
{
|
||||
copiedNotDeleted.incrementCountAndAddPrimaryKey(record.getValue("id"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
done.incrementCountAndAddPrimaryKey(record.getValue("id"));
|
||||
output.getRecords().add(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Result lines.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void addLines(ArrayList<ProcessSummaryLineInterface> lines)
|
||||
{
|
||||
done.addSelfToListIfAnyCount(lines);
|
||||
copiedNotDeleted.addSelfToListIfAnyCount(lines);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Copy, execute.
|
||||
*******************************************************************************/
|
||||
public static class CopyLoad extends CopyMoveLoad
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Constructor.
|
||||
*******************************************************************************/
|
||||
public CopyLoad()
|
||||
{
|
||||
super(EtlSupport.line(Status.OK, "object will be copied", "objects will be copied", "object was copied", "objects were copied"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Not a move.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected boolean isMove()
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Move, execute.
|
||||
*******************************************************************************/
|
||||
public static class MoveLoad extends CopyMoveLoad
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Constructor.
|
||||
*******************************************************************************/
|
||||
public MoveLoad()
|
||||
{
|
||||
super(EtlSupport.line(Status.OK, "object will be moved", "objects will be moved", "object was moved", "objects were moved"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A move.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected boolean isMove()
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
}
|
||||
}
|
||||
-392
@@ -1,392 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.processes;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.objectstorage.api.DeleteMode;
|
||||
import com.kingsrook.qbits.objectstorage.api.DeleteRequest;
|
||||
import com.kingsrook.qbits.objectstorage.api.DeleteResult;
|
||||
import com.kingsrook.qbits.objectstorage.api.StorageBucket;
|
||||
import com.kingsrook.qbits.objectstorage.config.ProviderType;
|
||||
import com.kingsrook.qbits.objectstorage.config.StorageBucketConfig;
|
||||
import com.kingsrook.qbits.objectstorage.internal.ObjectStorageRuntime;
|
||||
import com.kingsrook.qbits.objectstorage.internal.ProtectionRules;
|
||||
import com.kingsrook.qbits.objectstorage.model.ObjectStorageTables;
|
||||
import com.kingsrook.qbits.objectstorage.permissions.ObjectStoragePermissions;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.Status;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** "Delete objects" and "Restore objects". A delete is soft wherever the bucket
|
||||
** allows it (a delete marker on a versioned bucket, a move to the private
|
||||
** trash otherwise) and permanent only with object_storage.admin. Referenced
|
||||
** objects, protected objects and objects under object lock are skipped, and the
|
||||
** Review says why.
|
||||
*******************************************************************************/
|
||||
public final class DeleteRestoreProcesses
|
||||
{
|
||||
public static final String DELETE_NAME = "objectStorageDeleteObjects";
|
||||
public static final String RESTORE_NAME = "objectStorageRestoreObjects";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private DeleteRestoreProcesses()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The processes.
|
||||
*******************************************************************************/
|
||||
public static List<QProcessMetaData> processes()
|
||||
{
|
||||
return (List.of(
|
||||
EtlSupport.etl(DELETE_NAME, "Delete objects", "delete", ObjectStorageTables.OBJECT, DeleteTransform.class, DeleteLoad.class,
|
||||
"This is a preview of the objects that will be deleted. Referenced, protected and locked objects are skipped.", ObjectStoragePermissions.DELETE),
|
||||
EtlSupport.etl(RESTORE_NAME, "Restore objects", "restore_from_trash", ObjectStorageTables.OBJECT, RestoreTransform.class, RestoreLoad.class,
|
||||
"This is a preview of the deleted objects that will be restored.", ObjectStoragePermissions.WRITE)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Delete, review.
|
||||
*******************************************************************************/
|
||||
public static class DeleteTransform extends BaseTransform
|
||||
{
|
||||
private final ProcessSummaryLine marker = EtlSupport.line(Status.OK, "object will be deleted (a delete marker is added; Restore undoes it)",
|
||||
"objects will be deleted (a delete marker is added; Restore undoes it)", "object was deleted (a delete marker was added; Restore undoes it)",
|
||||
"objects were deleted (delete markers were added; Restore undoes it)");
|
||||
private final ProcessSummaryLine trash = EtlSupport.line(Status.OK, "object will be moved to the trash (Restore undoes it)", "objects will be moved to the trash (Restore undoes it)",
|
||||
"object was moved to the trash (Restore undoes it)", "objects were moved to the trash (Restore undoes it)");
|
||||
private final ProcessSummaryLine permanent = EtlSupport.line(Status.WARNING, "object will be deleted permanently (this cannot be undone)",
|
||||
"objects will be deleted permanently (this cannot be undone)", "object was deleted permanently", "objects were deleted permanently");
|
||||
private final ProcessSummaryLine needsAdmin = EtlSupport.line(Status.WARNING, "object is skipped: this bucket has no versioning or trash, so the delete would be permanent and needs object_storage.admin",
|
||||
"objects are skipped: these buckets have no versioning or trash, so the delete would be permanent and needs object_storage.admin",
|
||||
"object was skipped: a permanent delete needs object_storage.admin", "objects were skipped: a permanent delete needs object_storage.admin");
|
||||
private final ProcessSummaryLine referenced = EtlSupport.line(Status.WARNING, "object is skipped: it is referenced", "objects are skipped: they are referenced",
|
||||
"object was skipped: it is referenced", "objects were skipped: they are referenced");
|
||||
private final ProcessSummaryLine protectedLine = EtlSupport.line(Status.WARNING, "object is skipped: it is protected", "objects are skipped: they are protected",
|
||||
"object was skipped: it is protected", "objects were skipped: they are protected");
|
||||
private final ProcessSummaryLine locked = EtlSupport.line(Status.WARNING, "object is skipped: it is under object lock", "objects are skipped: they are under object lock",
|
||||
"object was skipped: it is under object lock", "objects were skipped: they are under object lock");
|
||||
private final ProcessSummaryLine backup = EtlSupport.line(Status.INFO, "object is skipped: it is in a backup bucket", "objects are skipped: they are in backup buckets",
|
||||
"object was skipped: it is in a backup bucket", "objects were skipped: they are in backup buckets");
|
||||
private final ProcessSummaryLine already = EtlSupport.line(Status.INFO, "object is already deleted (no change)", "objects are already deleted (no change)",
|
||||
"object was already deleted (no change)", "objects were already deleted (no change)");
|
||||
|
||||
private final Map<String, DeleteMode> modes = new HashMap<>();
|
||||
private boolean wasabi = false;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Classify one object.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void handle(QRecord record, RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
java.io.Serializable id = record.getValue("id");
|
||||
if(Boolean.TRUE.equals(record.getValueBoolean("deleted")))
|
||||
{
|
||||
already.incrementCountAndAddPrimaryKey(id);
|
||||
return;
|
||||
}
|
||||
ActionSupport.Classification c = ActionSupport.classify(record, ProtectionRules.Op.DELETE, true, true);
|
||||
switch(c.skip())
|
||||
{
|
||||
case REFERENCED -> skip(referenced, id, record, c);
|
||||
case PROTECTED -> skip(protectedLine, id, record, c);
|
||||
case LOCKED -> skip(locked, id, record, c);
|
||||
case BACKUP_BUCKET -> backup.incrementCountAndAddPrimaryKey(id);
|
||||
case NO_BUCKET_ACCESS -> noAccess.incrementCountAndAddPrimaryKey(id);
|
||||
default ->
|
||||
{
|
||||
String alias = ActionSupport.aliasOf(record);
|
||||
DeleteMode mode = modes.computeIfAbsent(alias, a -> ActionSupport.storage(a).plannedDeleteMode());
|
||||
StorageBucketConfig config = ActionSupport.config(alias);
|
||||
wasabi = wasabi || ObjectStorageRuntime.getConfig().findConnection(config.getConnection()).getProviderType() == ProviderType.WASABI;
|
||||
if(mode == DeleteMode.PERMANENT && !ActionSupport.isAdmin())
|
||||
{
|
||||
needsAdmin.incrementCountAndAddPrimaryKey(id);
|
||||
return;
|
||||
}
|
||||
(mode == DeleteMode.SOFT_DELETE_MARKER ? marker : mode == DeleteMode.SOFT_TRASH ? trash : permanent).incrementCountAndAddPrimaryKey(id);
|
||||
record.setValue("_mode", mode.name());
|
||||
output.getRecords().add(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Count a skip and keep the first few reasons as bullets.
|
||||
*******************************************************************************/
|
||||
private void skip(ProcessSummaryLine line, java.io.Serializable id, QRecord record, ActionSupport.Classification c)
|
||||
{
|
||||
line.incrementCountAndAddPrimaryKey(id);
|
||||
if(line.getBulletsOfText() == null)
|
||||
{
|
||||
line.setBulletsOfText(new ArrayList<>());
|
||||
}
|
||||
if(line.getBulletsOfText().size() < 5)
|
||||
{
|
||||
line.getBulletsOfText().add(record.getValueString("objectKey") + ": " + c.detail());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The lines.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void addLines(ArrayList<ProcessSummaryLineInterface> lines)
|
||||
{
|
||||
marker.addSelfToListIfAnyCount(lines);
|
||||
trash.addSelfToListIfAnyCount(lines);
|
||||
permanent.addSelfToListIfAnyCount(lines);
|
||||
needsAdmin.addSelfToListIfAnyCount(lines);
|
||||
referenced.addSelfToListIfAnyCount(lines);
|
||||
protectedLine.addSelfToListIfAnyCount(lines);
|
||||
locked.addSelfToListIfAnyCount(lines);
|
||||
backup.addSelfToListIfAnyCount(lines);
|
||||
already.addSelfToListIfAnyCount(lines);
|
||||
if(wasabi)
|
||||
{
|
||||
lines.add(EtlSupport.text(Status.INFO, "Wasabi bills deleted objects for their minimum storage duration, so deleting does not reduce the bill at once."));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The skips, as the Result screen shows them.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected List<ProcessSummaryLine> skipLines()
|
||||
{
|
||||
return (List.of(needsAdmin, referenced, protectedLine, locked, backup, already));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Delete, execute.
|
||||
*******************************************************************************/
|
||||
public static class DeleteLoad extends BaseLoad
|
||||
{
|
||||
private final ProcessSummaryLine marker = EtlSupport.line(Status.OK, "object will be deleted (a delete marker is added; Restore undoes it)",
|
||||
"objects will be deleted (a delete marker is added; Restore undoes it)", "object was deleted (a delete marker was added; Restore undoes it)",
|
||||
"objects were deleted (delete markers were added; Restore undoes it)");
|
||||
private final ProcessSummaryLine trash = EtlSupport.line(Status.OK, "object will be moved to the trash", "objects will be moved to the trash", "object was moved to the trash (Restore undoes it)",
|
||||
"objects were moved to the trash (Restore undoes it)");
|
||||
private final ProcessSummaryLine permanent = EtlSupport.line(Status.WARNING, "object will be deleted permanently", "objects will be deleted permanently", "object was deleted permanently",
|
||||
"objects were deleted permanently");
|
||||
private final ProcessSummaryLine lockedNow = EtlSupport.line(Status.WARNING, "object was skipped: object lock", "objects were skipped: object lock", "object was skipped: object lock",
|
||||
"objects were skipped: object lock");
|
||||
private final ProcessSummaryLine gone = EtlSupport.line(Status.INFO, "object was already gone", "objects were already gone", "object was already gone", "objects were already gone");
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Delete one object through the service (which logs it, updates the index and
|
||||
** tells the listeners).
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void apply(QRecord record, RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
String alias = ActionSupport.aliasOf(record);
|
||||
if("PERMANENT".equals(record.getValueString("_mode")) && !ActionSupport.isAdmin())
|
||||
{
|
||||
throw (new com.kingsrook.qbits.objectstorage.api.StorageException(com.kingsrook.qbits.objectstorage.api.StorageException.Reason.ACCESS_DENIED, "a permanent delete needs object_storage.admin"));
|
||||
}
|
||||
DeleteResult result = ActionSupport.storage(alias).delete(DeleteRequest.of(record.getValueString("objectKey")));
|
||||
switch(result.getMode())
|
||||
{
|
||||
case SOFT_DELETE_MARKER -> marker.incrementCountAndAddPrimaryKey(record.getValue("id"));
|
||||
case SOFT_TRASH -> trash.incrementCountAndAddPrimaryKey(record.getValue("id"));
|
||||
case PERMANENT -> permanent.incrementCountAndAddPrimaryKey(record.getValue("id"));
|
||||
case SKIPPED_OBJECT_LOCK -> lockedNow.incrementCountAndAddPrimaryKey(record.getValue("id"));
|
||||
default -> gone.incrementCountAndAddPrimaryKey(record.getValue("id"));
|
||||
}
|
||||
output.getRecords().add(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Result lines.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void addLines(ArrayList<ProcessSummaryLineInterface> lines)
|
||||
{
|
||||
marker.addSelfToListIfAnyCount(lines);
|
||||
trash.addSelfToListIfAnyCount(lines);
|
||||
permanent.addSelfToListIfAnyCount(lines);
|
||||
lockedNow.addSelfToListIfAnyCount(lines);
|
||||
gone.addSelfToListIfAnyCount(lines);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Restore, review.
|
||||
*******************************************************************************/
|
||||
public static class RestoreTransform extends BaseTransform
|
||||
{
|
||||
private final ProcessSummaryLine unmark = EtlSupport.line(Status.OK, "delete marker will be removed", "delete markers will be removed", "delete marker was removed", "delete markers were removed");
|
||||
private final ProcessSummaryLine fromTrash = EtlSupport.line(Status.OK, "object will be copied back from the trash", "objects will be copied back from the trash",
|
||||
"object was copied back from the trash", "objects were copied back from the trash");
|
||||
private final ProcessSummaryLine notDeleted = EtlSupport.line(Status.INFO, "object is not deleted (no change)", "objects are not deleted (no change)",
|
||||
"object was not deleted (no change)", "objects were not deleted (no change)");
|
||||
private final ProcessSummaryLine cannot = EtlSupport.line(Status.WARNING, "object cannot be restored: its bucket has no versioning or trash", "objects cannot be restored: their buckets have no versioning or trash",
|
||||
"object could not be restored: its bucket has no versioning or trash", "objects could not be restored: their buckets have no versioning or trash");
|
||||
private final ProcessSummaryLine protectedLine = EtlSupport.line(Status.WARNING, "object is skipped: it is protected", "objects are skipped: they are protected",
|
||||
"object was skipped: it is protected", "objects were skipped: they are protected");
|
||||
private final Map<String, DeleteMode> modes = new HashMap<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Classify one object.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void handle(QRecord record, RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
java.io.Serializable id = record.getValue("id");
|
||||
if(!Boolean.TRUE.equals(record.getValueBoolean("deleted")))
|
||||
{
|
||||
notDeleted.incrementCountAndAddPrimaryKey(id);
|
||||
return;
|
||||
}
|
||||
String alias = ActionSupport.aliasOf(record);
|
||||
if(ProtectionRules.check(alias, record.getValueString("objectKey"), ProtectionRules.Op.WRITE, com.kingsrook.qbits.objectstorage.api.StorageCaller.user()).blocked())
|
||||
{
|
||||
protectedLine.incrementCountAndAddPrimaryKey(id);
|
||||
return;
|
||||
}
|
||||
DeleteMode mode = modes.computeIfAbsent(alias, a -> ActionSupport.storage(a).plannedDeleteMode());
|
||||
if(mode == DeleteMode.SOFT_DELETE_MARKER)
|
||||
{
|
||||
unmark.incrementCountAndAddPrimaryKey(id);
|
||||
}
|
||||
else if(mode == DeleteMode.SOFT_TRASH)
|
||||
{
|
||||
fromTrash.incrementCountAndAddPrimaryKey(id);
|
||||
}
|
||||
else
|
||||
{
|
||||
cannot.incrementCountAndAddPrimaryKey(id);
|
||||
return;
|
||||
}
|
||||
output.getRecords().add(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The lines.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void addLines(ArrayList<ProcessSummaryLineInterface> lines)
|
||||
{
|
||||
unmark.addSelfToListIfAnyCount(lines);
|
||||
fromTrash.addSelfToListIfAnyCount(lines);
|
||||
notDeleted.addSelfToListIfAnyCount(lines);
|
||||
cannot.addSelfToListIfAnyCount(lines);
|
||||
protectedLine.addSelfToListIfAnyCount(lines);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The skips, as the Result screen shows them.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected List<ProcessSummaryLine> skipLines()
|
||||
{
|
||||
return (List.of(notDeleted, cannot, protectedLine));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Restore, execute.
|
||||
*******************************************************************************/
|
||||
public static class RestoreLoad extends BaseLoad
|
||||
{
|
||||
private final ProcessSummaryLine restored = EtlSupport.line(Status.OK, "object will be restored", "objects will be restored", "object was restored", "objects were restored");
|
||||
private final ProcessSummaryLine nothing = EtlSupport.line(Status.INFO, "object had nothing to restore (it may have been purged)", "objects had nothing to restore (they may have been purged)",
|
||||
"object had nothing to restore (it may have been purged)", "objects had nothing to restore (they may have been purged)");
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Restore one object.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void apply(QRecord record, RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
StorageBucket storage = ActionSupport.storage(ActionSupport.aliasOf(record));
|
||||
if(storage.restore(record.getValueString("objectKey")))
|
||||
{
|
||||
restored.incrementCountAndAddPrimaryKey(record.getValue("id"));
|
||||
output.getRecords().add(record);
|
||||
}
|
||||
else
|
||||
{
|
||||
nothing.incrementCountAndAddPrimaryKey(record.getValue("id"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Result lines.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void addLines(ArrayList<ProcessSummaryLineInterface> lines)
|
||||
{
|
||||
restored.addSelfToListIfAnyCount(lines);
|
||||
nothing.addSelfToListIfAnyCount(lines);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.processes;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.objectstorage.internal.ObjectStorageRuntime;
|
||||
import com.kingsrook.qbits.objectstorage.model.ObjectStorageTables;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.Status;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractExtractStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractLoadStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractTransformStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Builds the qbit's processes as standard streamed ETLs: record input from the
|
||||
** selected rows, the current filter or all rows; a transform step that produces
|
||||
** the Review summary lines; a load step that does the work; and the standard
|
||||
** Result. Processes are table or record actions, never in the nav.
|
||||
*******************************************************************************/
|
||||
public final class EtlSupport
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private EtlSupport()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A standard streamed ETL process over a table, with full validation (the
|
||||
** Review step is the dry run).
|
||||
*******************************************************************************/
|
||||
public static QProcessMetaData etl(String name, String label, String icon, String table, Class<? extends AbstractTransformStep> transform,
|
||||
Class<? extends AbstractLoadStep> load, String preview, String permission)
|
||||
{
|
||||
return (etl(name, label, icon, table, table, ExtractViaQueryStep.class, transform, load, preview, permission));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A streamed ETL whose records come from a custom extract step (for example the
|
||||
** objects under selected folders), shown as an action on another table.
|
||||
*******************************************************************************/
|
||||
public static QProcessMetaData etl(String name, String label, String icon, String recordTable, String actionTable, Class<? extends AbstractExtractStep> extract,
|
||||
Class<? extends AbstractTransformStep> transform, Class<? extends AbstractLoadStep> load, String preview, String permission)
|
||||
{
|
||||
Map<String, Serializable> values = new HashMap<>();
|
||||
values.put(StreamedETLWithFrontendProcess.FIELD_SOURCE_TABLE, recordTable);
|
||||
values.put(StreamedETLWithFrontendProcess.FIELD_DESTINATION_TABLE, recordTable);
|
||||
values.put(StreamedETLWithFrontendProcess.FIELD_SUPPORTS_FULL_VALIDATION, true);
|
||||
values.put(StreamedETLWithFrontendProcess.FIELD_DO_FULL_VALIDATION, true);
|
||||
values.put(StreamedETLWithFrontendProcess.FIELD_PREVIEW_MESSAGE, preview);
|
||||
QProcessMetaData process = StreamedETLWithFrontendProcess.defineProcessMetaData(extract, transform, load, values);
|
||||
process.setName(name);
|
||||
String table = actionTable;
|
||||
process.setLabel(label);
|
||||
process.setIcon(new QIcon(icon));
|
||||
process.setTableName(table);
|
||||
process.setPermissionRules(ObjectStorageTables.permissionRules());
|
||||
ObjectStorageRuntime.processPermissions().put(name, permission);
|
||||
return (process);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A summary line that is plain text with no count in front of it (the standard
|
||||
** line starts at a count of 0, which the screen would show as "0 ...").
|
||||
*******************************************************************************/
|
||||
public static ProcessSummaryLine text(Status status, String message)
|
||||
{
|
||||
ProcessSummaryLine line = new ProcessSummaryLine(status, message);
|
||||
line.setCount(null);
|
||||
return (line);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Record the permission a process needs.
|
||||
*******************************************************************************/
|
||||
public static void permission(String processName, String permission)
|
||||
{
|
||||
ObjectStorageRuntime.processPermissions().put(processName, permission);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Put an input form (help text plus fields) in front of an action, the standard
|
||||
** way a QQQ process asks for values before its Review.
|
||||
*******************************************************************************/
|
||||
public static QProcessMetaData withInput(QProcessMetaData process, String label, String help, QFieldMetaData... fields)
|
||||
{
|
||||
QFrontendStepMetaData input = new QFrontendStepMetaData().withName("input").withLabel(label).withFormFields(List.of(fields))
|
||||
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.HELP_TEXT).withValue("text", help))
|
||||
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM));
|
||||
process.withStep(0, input);
|
||||
return (process);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A string field for an input form.
|
||||
*******************************************************************************/
|
||||
public static QFieldMetaData stringField(String name, String label, boolean required)
|
||||
{
|
||||
return (new QFieldMetaData(name, QFieldType.STRING).withLabel(label).withIsRequired(required));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A boolean field for an input form.
|
||||
*******************************************************************************/
|
||||
public static QFieldMetaData booleanField(String name, String label, boolean defaultValue)
|
||||
{
|
||||
return (new QFieldMetaData(name, QFieldType.BOOLEAN).withLabel(label).withDefaultValue(defaultValue));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A summary line with future (Review) and past (Result) wording.
|
||||
*******************************************************************************/
|
||||
public static ProcessSummaryLine line(Status status, String singularFuture, String pluralFuture, String singularPast, String pluralPast)
|
||||
{
|
||||
ProcessSummaryLine line = new ProcessSummaryLine(status, 0, null);
|
||||
line.setSingularFutureMessage(singularFuture);
|
||||
line.setPluralFutureMessage(pluralFuture);
|
||||
line.setSingularPastMessage(singularPast);
|
||||
line.setPluralPastMessage(pluralPast);
|
||||
return (line);
|
||||
}
|
||||
}
|
||||
@@ -1,243 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.objectstorage.processes;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.objectstorage.api.StorageKeys;
|
||||
import com.kingsrook.qbits.objectstorage.model.ObjectStorageTables;
|
||||
import com.kingsrook.qbits.objectstorage.permissions.ObjectStoragePermissions;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.Status;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Folder record actions: "Move or rename folder" and "Delete folder contents".
|
||||
** A folder is a prefix, so these act on the objects under the selected
|
||||
** folders, with exactly the same Review lines, skip rules and Result as the
|
||||
** object actions (referenced, protected and locked objects are skipped).
|
||||
*******************************************************************************/
|
||||
public final class FolderProcesses
|
||||
{
|
||||
public static final String RENAME_NAME = "objectStorageRenameFolder";
|
||||
public static final String DELETE_NAME = "objectStorageDeleteFolder";
|
||||
|
||||
public static final String FIELD_NEW_PATH = "newPath";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private FolderProcesses()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The processes.
|
||||
*******************************************************************************/
|
||||
public static List<QProcessMetaData> processes()
|
||||
{
|
||||
return (List.of(
|
||||
EtlSupport.withInput(EtlSupport.etl(RENAME_NAME, "Move or rename folder", "drive_file_rename_outline", ObjectStorageTables.OBJECT, ObjectStorageTables.FOLDER, FolderObjectsExtractStep.class,
|
||||
FolderMoveTransform.class, CopyMoveProcesses.MoveLoad.class, "This is a preview of the objects under the folder that will be moved.", ObjectStoragePermissions.WRITE),
|
||||
"Move or rename folder", "Every object under the folder moves to the new folder path. A move is a copy followed by a delete, not one atomic step.",
|
||||
EtlSupport.stringField(FIELD_NEW_PATH, "New folder path (for example media/archive/2026/)", true),
|
||||
new QFieldMetaData(CopyMoveProcesses.FIELD_DEST_BUCKET, QFieldType.INTEGER).withLabel("Destination bucket (leave blank for the same bucket)").withPossibleValueSourceName(ObjectStorageTables.BUCKET),
|
||||
EtlSupport.booleanField(CopyMoveProcesses.FIELD_OVERWRITE, "Overwrite objects that already exist at the destination", false)),
|
||||
EtlSupport.etl(DELETE_NAME, "Delete folder contents", "folder_delete", ObjectStorageTables.OBJECT, ObjectStorageTables.FOLDER, FolderObjectsExtractStep.class,
|
||||
DeleteRestoreProcesses.DeleteTransform.class, DeleteRestoreProcesses.DeleteLoad.class, "This is a preview of the objects under the folder that will be deleted. Referenced, protected and locked objects are skipped.",
|
||||
ObjectStoragePermissions.DELETE)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Extracts the live objects under the selected folders (selected rows, the
|
||||
** current filter, or all folders): the folder selection is read as the user
|
||||
** (so per-bucket locks apply), then the object rows under those prefixes.
|
||||
*******************************************************************************/
|
||||
public static class FolderObjectsExtractStep extends ExtractViaQueryStep
|
||||
{
|
||||
/*******************************************************************************
|
||||
** The folders the user selected.
|
||||
*******************************************************************************/
|
||||
public List<QRecord> selectedFolders(RunBackendStepInput input) throws QException
|
||||
{
|
||||
QQueryFilter folderFilter = super.getQueryFilter(input);
|
||||
return (new QueryAction().execute(new QueryInput(ObjectStorageTables.FOLDER).withFilter(folderFilter)).getRecords());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The object filter: live objects under any selected folder (a folder inside
|
||||
** another selected folder adds nothing).
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected QQueryFilter getQueryFilter(RunBackendStepInput input) throws QException
|
||||
{
|
||||
List<QRecord> folders = selectedFolders(input);
|
||||
List<QRecord> top = new ArrayList<>();
|
||||
for(QRecord folder : folders)
|
||||
{
|
||||
boolean nested = false;
|
||||
for(QRecord other : folders)
|
||||
{
|
||||
if(other != folder && other.getValueInteger("bucketId").equals(folder.getValueInteger("bucketId")) && folder.getValueString("path").startsWith(other.getValueString("path"))
|
||||
&& !folder.getValueString("path").equals(other.getValueString("path")))
|
||||
{
|
||||
nested = true;
|
||||
}
|
||||
}
|
||||
if(!nested)
|
||||
{
|
||||
top.add(folder);
|
||||
}
|
||||
}
|
||||
if(top.isEmpty())
|
||||
{
|
||||
return (new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.IN, List.of(-1))));
|
||||
}
|
||||
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR);
|
||||
for(QRecord folder : top)
|
||||
{
|
||||
filter.withSubFilter(new QQueryFilter(new QFilterCriteria("bucketId", QCriteriaOperator.EQUALS, folder.getValueInteger("bucketId")),
|
||||
new QFilterCriteria("deleted", QCriteriaOperator.EQUALS, false), new QFilterCriteria("objectKey", QCriteriaOperator.STARTS_WITH, folder.getValueString("path"))));
|
||||
}
|
||||
return (filter);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Move or rename folder, review: the objects move under the new folder path.
|
||||
*******************************************************************************/
|
||||
public static class FolderMoveTransform extends CopyMoveProcesses.CopyMoveTransform
|
||||
{
|
||||
private List<QRecord> folders;
|
||||
private String error;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor.
|
||||
*******************************************************************************/
|
||||
public FolderMoveTransform()
|
||||
{
|
||||
super(EtlSupport.line(Status.OK, "object will be moved", "objects will be moved", "object was moved", "objects were moved"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A move.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected boolean isMove()
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The object's new key: the new folder path plus what followed the old path.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected String destKeyFor(QRecord record, RunBackendStepInput input)
|
||||
{
|
||||
try
|
||||
{
|
||||
if(folders == null)
|
||||
{
|
||||
folders = new FolderObjectsExtractStep().selectedFolders(input);
|
||||
}
|
||||
}
|
||||
catch(QException e)
|
||||
{
|
||||
throw (new IllegalArgumentException("Could not read the selected folders"));
|
||||
}
|
||||
if(folders.size() != 1)
|
||||
{
|
||||
error = "Rename works on one folder at a time; " + folders.size() + " are selected.";
|
||||
throw (new IllegalArgumentException(error));
|
||||
}
|
||||
String oldPath = folders.get(0).getValueString("path");
|
||||
String newPath = StorageKeys.normalizePrefix(input.getValueString(FIELD_NEW_PATH));
|
||||
if(newPath.isEmpty())
|
||||
{
|
||||
error = "A new folder path is required.";
|
||||
throw (new IllegalArgumentException(error));
|
||||
}
|
||||
if(!newPath.endsWith("/"))
|
||||
{
|
||||
newPath = newPath + "/";
|
||||
}
|
||||
return (StorageKeys.normalize(newPath + record.getValueString("objectKey").substring(oldPath.length())));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Review lines, with the input problem first.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void addLines(ArrayList<ProcessSummaryLineInterface> lines)
|
||||
{
|
||||
if(error != null)
|
||||
{
|
||||
lines.add(EtlSupport.text(Status.ERROR, error));
|
||||
}
|
||||
super.addLines(lines);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The ids of folders, for tests.
|
||||
*******************************************************************************/
|
||||
static List<Serializable> idsOf(List<QRecord> folders)
|
||||
{
|
||||
List<Serializable> ids = new ArrayList<>();
|
||||
folders.forEach(f -> ids.add(f.getValue("id")));
|
||||
return (ids);
|
||||
}
|
||||
}
|
||||
Loaded 100 of 133 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user