textus 0.51.0 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +19 -19
- data/SPEC.md +41 -39
- data/docs/architecture/README.md +9 -9
- data/docs/reference/conventions.md +8 -8
- data/lib/textus/boot.rb +7 -5
- data/lib/textus/cli/runner.rb +2 -2
- data/lib/textus/cli/verb/put.rb +1 -1
- data/lib/textus/cli/verb/serve.rb +19 -0
- data/lib/textus/dispatcher.rb +3 -1
- data/lib/textus/doctor/check/generator_drift.rb +1 -1
- data/lib/textus/doctor/check/sentinels.rb +2 -2
- data/lib/textus/domain/freshness/evaluator.rb +2 -2
- data/lib/textus/domain/jobs/job.rb +58 -0
- data/lib/textus/domain/jobs/registry.rb +37 -0
- data/lib/textus/domain/policy/base_guards.rb +1 -1
- data/lib/textus/domain/policy/retention.rb +1 -1
- data/lib/textus/domain/policy/source.rb +4 -10
- data/lib/textus/errors.rb +2 -2
- data/lib/textus/hooks/catalog.rb +0 -1
- data/lib/textus/init/templates/machine_intake.rb +1 -1
- data/lib/textus/init.rb +4 -4
- data/lib/textus/jobs/handlers.rb +62 -0
- data/lib/textus/jobs/scheduler.rb +36 -0
- data/lib/textus/jobs/seeder.rb +57 -0
- data/lib/textus/layout.rb +8 -0
- data/lib/textus/maintenance/drain.rb +42 -0
- data/lib/textus/maintenance/retention/apply.rb +52 -0
- data/lib/textus/maintenance/serve.rb +30 -0
- data/lib/textus/maintenance/worker.rb +74 -0
- data/lib/textus/manifest/capabilities.rb +1 -1
- data/lib/textus/manifest/data.rb +16 -1
- data/lib/textus/manifest/schema/keys.rb +1 -1
- data/lib/textus/manifest/schema/validator.rb +3 -3
- data/lib/textus/manifest/schema/vocabulary.rb +2 -2
- data/lib/textus/mcp/server.rb +1 -1
- data/lib/textus/ports/build_lock.rb +1 -1
- data/lib/textus/ports/produce_on_write_subscriber.rb +28 -24
- data/lib/textus/ports/queue.rb +130 -0
- data/lib/textus/produce/acquire/handler.rb +1 -1
- data/lib/textus/produce/acquire/intake.rb +3 -3
- data/lib/textus/produce/engine.rb +10 -58
- data/lib/textus/produce/events.rb +1 -1
- data/lib/textus/read/freshness.rb +2 -2
- data/lib/textus/read/get.rb +3 -3
- data/lib/textus/read/jobs.rb +31 -0
- data/lib/textus/role.rb +1 -1
- data/lib/textus/version.rb +1 -1
- data/lib/textus/write/enqueue.rb +50 -0
- metadata +14 -2
- data/lib/textus/maintenance/reconcile.rb +0 -160
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
module Produce
|
|
3
3
|
# Single home for the fetch lifecycle event vocabulary (ADR 0048 D5).
|
|
4
|
-
# Produce::Acquire::Intake (the ingest executor driven by
|
|
4
|
+
# Produce::Acquire::Intake (the ingest executor driven by converge + hook) emits through
|
|
5
5
|
# this seam so the event names and payload shapes live in one place with one
|
|
6
6
|
# derived hook context.
|
|
7
7
|
class Events
|
|
@@ -10,7 +10,7 @@ module Textus
|
|
|
10
10
|
# - entries matched by a `retention:` rule: `retention.ttl_seconds` is the
|
|
11
11
|
# GC age; basis = file mtime. Past ttl ⇒ :expired (:action = drop/archive).
|
|
12
12
|
# Intake cadence wins when both apply (freshness is content currency; GC dueness
|
|
13
|
-
# shows via `
|
|
13
|
+
# shows via `drain --dry-run`).
|
|
14
14
|
# Status is one of :fresh, :expired, or :no_policy; the row also carries
|
|
15
15
|
# :action (:refresh for intake, :drop/:archive for retention).
|
|
16
16
|
#
|
|
@@ -78,7 +78,7 @@ module Textus
|
|
|
78
78
|
# ADR 0093: staleness comes from the intake re-pull cadence (source.ttl)
|
|
79
79
|
# or a retention GC rule (retention.ttl). Intake cadence wins when an entry
|
|
80
80
|
# has both (freshness is about content currency; GC dueness still shows via
|
|
81
|
-
# `
|
|
81
|
+
# `drain --dry-run`). Returns [ttl_seconds, action] or [nil, nil].
|
|
82
82
|
def policy_for(mentry)
|
|
83
83
|
if mentry.intake?
|
|
84
84
|
ttl = mentry.source.ttl_seconds
|
data/lib/textus/read/get.rb
CHANGED
|
@@ -2,16 +2,16 @@ module Textus
|
|
|
2
2
|
module Read
|
|
3
3
|
# The one read path — a pure read (ADR 0089, 0093): the on-disk envelope
|
|
4
4
|
# annotated with a freshness annotation. It NEVER mutates and NEVER ingests.
|
|
5
|
-
# Quarantine freshness is system-pushed via `
|
|
5
|
+
# Quarantine freshness is system-pushed via `drain` (scheduled sweep) and
|
|
6
6
|
# `hook run` (event push). Lifecycle is removed from the get path (ADR 0093):
|
|
7
7
|
# intake cadence lives in `source.ttl`; GC lives in `retention:` rules; both
|
|
8
|
-
# are evaluated exclusively by the `
|
|
8
|
+
# are evaluated exclusively by the `drain` sweep, not by a read.
|
|
9
9
|
class Get
|
|
10
10
|
extend Textus::Contract::DSL
|
|
11
11
|
|
|
12
12
|
verb :get
|
|
13
13
|
summary "Read one entry — a pure on-disk read annotated with a freshness " \
|
|
14
|
-
"verdict; never ingests (quarantine freshness is
|
|
14
|
+
"verdict; never ingests (quarantine freshness is drain + hook " \
|
|
15
15
|
"only, ADR 0089). Returns the envelope (uid, etag, _meta, body, " \
|
|
16
16
|
"freshness)."
|
|
17
17
|
surfaces :cli, :mcp
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Read
|
|
3
|
+
# Inspect and operate the job queue: list ids by state, retry a dead-lettered
|
|
4
|
+
# job, or purge a state. The agent's window into deferred convergence work.
|
|
5
|
+
class Jobs
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :jobs
|
|
9
|
+
summary "List queued jobs by state; retry a dead-lettered job or purge a state."
|
|
10
|
+
surfaces :cli, :mcp
|
|
11
|
+
cli "jobs"
|
|
12
|
+
arg :state, String, default: "ready", description: "ready|leased|done|failed"
|
|
13
|
+
arg :action, String, default: nil, description: "retry|purge (optional)"
|
|
14
|
+
arg :job_id, String, default: nil, description: "job id (required for action=retry)"
|
|
15
|
+
|
|
16
|
+
def initialize(container:, call:)
|
|
17
|
+
@container = container
|
|
18
|
+
@call = call
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call(state: "ready", action: nil, job_id: nil)
|
|
22
|
+
queue = Textus::Ports::Queue.new(root: @container.root)
|
|
23
|
+
case action
|
|
24
|
+
when "retry" then queue.retry_failed(job_id)
|
|
25
|
+
when "purge" then queue.purge(state)
|
|
26
|
+
end
|
|
27
|
+
{ "protocol" => Textus::PROTOCOL, "ok" => true, "state" => state, "jobs" => queue.list(state) }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/textus/role.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
module Role
|
|
3
3
|
# The three role archetypes, each string sourced exactly once: human curates
|
|
4
|
-
# canon, agent proposes, automation
|
|
4
|
+
# canon, agent proposes, automation converges the machine-maintained lanes
|
|
5
5
|
# (refresh + materialize) (explanation/concepts.md).
|
|
6
6
|
# Reference these constants instead of bare literals (ADR 0044).
|
|
7
7
|
HUMAN = "human".freeze
|
data/lib/textus/version.rb
CHANGED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Write
|
|
3
|
+
# Push a job of a REGISTERED type onto the convergence queue, to be run by
|
|
4
|
+
# drain/serve. The closed allow-list (Jobs::Handlers.registry) is the safety
|
|
5
|
+
# boundary: an unregistered type is refused, so the general runner can never
|
|
6
|
+
# execute arbitrary code. Authority is checked here (the caller must hold the
|
|
7
|
+
# type's required_role, if any) and frozen onto the job's `enqueued_by` — the
|
|
8
|
+
# worker runs it as exactly this role, no escalation via the queue.
|
|
9
|
+
class Enqueue
|
|
10
|
+
extend Textus::Contract::DSL
|
|
11
|
+
|
|
12
|
+
verb :enqueue
|
|
13
|
+
summary "Push a registered job type onto the convergence queue, to be run by drain/serve."
|
|
14
|
+
surfaces :cli, :mcp
|
|
15
|
+
cli "enqueue"
|
|
16
|
+
arg :type, String, required: true, positional: true, description: "registered job type (e.g. materialize, re-pull, sweep)"
|
|
17
|
+
arg :args, Hash, positional: true, default: {}, description: "type-specific arguments (e.g. { key: ... } or { scope: ... })"
|
|
18
|
+
|
|
19
|
+
def initialize(container:, call:)
|
|
20
|
+
@container = container
|
|
21
|
+
@call = call
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call(type, args = {})
|
|
25
|
+
entry = Textus::Jobs::Handlers.registry.lookup(type) # raises UsageError for unregistered types
|
|
26
|
+
authorize!(entry)
|
|
27
|
+
|
|
28
|
+
job = Textus::Domain::Jobs::Job.new(
|
|
29
|
+
type: type, args: args, enqueued_by: @call.role, max_attempts: entry.max_attempts,
|
|
30
|
+
)
|
|
31
|
+
Textus::Ports::Queue.new(root: @container.root).enqueue(job)
|
|
32
|
+
{ "protocol" => Textus::PROTOCOL, "ok" => true, "id" => job.id }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def authorize!(entry)
|
|
38
|
+
required = entry.required_role
|
|
39
|
+
return if required.nil? || @call.role == required
|
|
40
|
+
|
|
41
|
+
raise Textus::Error.new(
|
|
42
|
+
"forbidden",
|
|
43
|
+
"role '#{@call.role}' is not authorized to enqueue this job type (requires '#{required}')",
|
|
44
|
+
details: { "role" => @call.role, "required_role" => required },
|
|
45
|
+
exit_code: 77,
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: textus
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.52.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick
|
|
@@ -132,6 +132,7 @@ files:
|
|
|
132
132
|
- lib/textus/cli/verb/schema_diff.rb
|
|
133
133
|
- lib/textus/cli/verb/schema_init.rb
|
|
134
134
|
- lib/textus/cli/verb/schema_migrate.rb
|
|
135
|
+
- lib/textus/cli/verb/serve.rb
|
|
135
136
|
- lib/textus/container.rb
|
|
136
137
|
- lib/textus/contract.rb
|
|
137
138
|
- lib/textus/contract/around.rb
|
|
@@ -167,6 +168,8 @@ files:
|
|
|
167
168
|
- lib/textus/domain/freshness.rb
|
|
168
169
|
- lib/textus/domain/freshness/evaluator.rb
|
|
169
170
|
- lib/textus/domain/freshness/verdict.rb
|
|
171
|
+
- lib/textus/domain/jobs/job.rb
|
|
172
|
+
- lib/textus/domain/jobs/registry.rb
|
|
170
173
|
- lib/textus/domain/permission.rb
|
|
171
174
|
- lib/textus/domain/policy/base_guards.rb
|
|
172
175
|
- lib/textus/domain/policy/evaluation.rb
|
|
@@ -210,16 +213,22 @@ files:
|
|
|
210
213
|
- lib/textus/init.rb
|
|
211
214
|
- lib/textus/init/templates/machine_intake.rb
|
|
212
215
|
- lib/textus/init/templates/orientation_reducer.rb
|
|
216
|
+
- lib/textus/jobs/handlers.rb
|
|
217
|
+
- lib/textus/jobs/scheduler.rb
|
|
218
|
+
- lib/textus/jobs/seeder.rb
|
|
213
219
|
- lib/textus/key/distance.rb
|
|
214
220
|
- lib/textus/key/grammar.rb
|
|
215
221
|
- lib/textus/key/matching.rb
|
|
216
222
|
- lib/textus/key/path.rb
|
|
217
223
|
- lib/textus/layout.rb
|
|
218
224
|
- lib/textus/maintenance.rb
|
|
225
|
+
- lib/textus/maintenance/drain.rb
|
|
219
226
|
- lib/textus/maintenance/key_delete_prefix.rb
|
|
220
227
|
- lib/textus/maintenance/key_mv_prefix.rb
|
|
221
|
-
- lib/textus/maintenance/
|
|
228
|
+
- lib/textus/maintenance/retention/apply.rb
|
|
222
229
|
- lib/textus/maintenance/rule_lint.rb
|
|
230
|
+
- lib/textus/maintenance/serve.rb
|
|
231
|
+
- lib/textus/maintenance/worker.rb
|
|
223
232
|
- lib/textus/maintenance/zone_mv.rb
|
|
224
233
|
- lib/textus/manifest.rb
|
|
225
234
|
- lib/textus/manifest/capabilities.rb
|
|
@@ -263,6 +272,7 @@ files:
|
|
|
263
272
|
- lib/textus/ports/clock.rb
|
|
264
273
|
- lib/textus/ports/produce_on_write_subscriber.rb
|
|
265
274
|
- lib/textus/ports/publisher.rb
|
|
275
|
+
- lib/textus/ports/queue.rb
|
|
266
276
|
- lib/textus/ports/sentinel_store.rb
|
|
267
277
|
- lib/textus/ports/storage/file_stat.rb
|
|
268
278
|
- lib/textus/ports/storage/file_store.rb
|
|
@@ -285,6 +295,7 @@ files:
|
|
|
285
295
|
- lib/textus/read/doctor.rb
|
|
286
296
|
- lib/textus/read/freshness.rb
|
|
287
297
|
- lib/textus/read/get.rb
|
|
298
|
+
- lib/textus/read/jobs.rb
|
|
288
299
|
- lib/textus/read/list.rb
|
|
289
300
|
- lib/textus/read/published.rb
|
|
290
301
|
- lib/textus/read/pulse.rb
|
|
@@ -306,6 +317,7 @@ files:
|
|
|
306
317
|
- lib/textus/uid.rb
|
|
307
318
|
- lib/textus/version.rb
|
|
308
319
|
- lib/textus/write/accept.rb
|
|
320
|
+
- lib/textus/write/enqueue.rb
|
|
309
321
|
- lib/textus/write/key_delete.rb
|
|
310
322
|
- lib/textus/write/key_mv.rb
|
|
311
323
|
- lib/textus/write/propose.rb
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Maintenance
|
|
5
|
-
# Two-phase convergence pass (ADR 0093). Replaces the old Lifecycle-reporter
|
|
6
|
-
# sweep.
|
|
7
|
-
#
|
|
8
|
-
# Phase 1 — Produce (non-destructive): re-render ALL derived entries (cheap,
|
|
9
|
-
# idempotent) plus every intake entry past its source.ttl (stale-only, so
|
|
10
|
-
# external sources are not hammered). Driven by Produce::Engine.
|
|
11
|
-
#
|
|
12
|
-
# Phase 2 — Retention sweep (destructive): drop or archive entries past their
|
|
13
|
-
# retention ttl. Driven by Domain::Retention::Sweep. The old refresh/warn
|
|
14
|
-
# actions are gone — intake re-pull is now Produce's responsibility.
|
|
15
|
-
class Reconcile
|
|
16
|
-
extend Textus::Contract::DSL
|
|
17
|
-
|
|
18
|
-
verb :reconcile
|
|
19
|
-
summary "Run the convergence pass: produce derived + stale intake, then drop/archive aged entries; report health."
|
|
20
|
-
surfaces :cli, :mcp
|
|
21
|
-
cli "reconcile"
|
|
22
|
-
arg :prefix, String, description: "restrict the sweep to keys under this dotted prefix"
|
|
23
|
-
arg :zone, String, description: "restrict the sweep to entries in this zone"
|
|
24
|
-
arg :dry_run, :boolean, default: false,
|
|
25
|
-
description: "when true, report what the pass WOULD do without applying; " \
|
|
26
|
-
"defaults to false, so omitting it produces + drops/archives immediately"
|
|
27
|
-
|
|
28
|
-
def initialize(container:, call:)
|
|
29
|
-
@container = container
|
|
30
|
-
@call = call
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def call(prefix: nil, zone: nil, dry_run: false)
|
|
34
|
-
file_stat = Textus::Ports::Storage::FileStat.new
|
|
35
|
-
retention_rows = Textus::Domain::Retention::Sweep.new(
|
|
36
|
-
manifest: @container.manifest, file_stat: file_stat, clock: Textus::Ports::Clock.new,
|
|
37
|
-
).call(prefix: prefix, zone: zone)
|
|
38
|
-
|
|
39
|
-
produce_keys = produce_scope(prefix, zone, file_stat)
|
|
40
|
-
health = Read::Doctor.new(container: @container, call: @call).call
|
|
41
|
-
return dry_run_result(produce_keys, retention_rows, health) if dry_run
|
|
42
|
-
|
|
43
|
-
# reconcile is the authoritative "make everything current now" pass, so
|
|
44
|
-
# it subsumes any in-flight reactive produce: drain pending async
|
|
45
|
-
# produce-on-write threads first, both to fold their work in and to free
|
|
46
|
-
# the shared maintenance lock (BuildLock is non-blocking — a thread still
|
|
47
|
-
# holding it would make the acquire below raise BuildInProgress). ADR 0093.
|
|
48
|
-
Textus::Produce::Engine::AsyncRunner.drain
|
|
49
|
-
|
|
50
|
-
Textus::Ports::BuildLock.with(root: @container.root) do
|
|
51
|
-
produced = Textus::Produce::Engine.new(container: @container, call: @call).call(keys: produce_keys)
|
|
52
|
-
swept = apply(retention_rows)
|
|
53
|
-
publish_failed(swept[:failed]) unless swept[:failed].empty?
|
|
54
|
-
apply_result(produced, swept, health)
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
private
|
|
59
|
-
|
|
60
|
-
# The full produce scope (ADR 0093): every derived entry (always
|
|
61
|
-
# re-render — cheap, idempotent), every entry that mirrors a publish_tree
|
|
62
|
-
# (the nested-subtree publishers, ADR 0047 — mirrored each pass so a
|
|
63
|
-
# removed source leaf is swept from the published tree), every authored
|
|
64
|
-
# leaf with a `publish.to` target (the single-file canon publishers —
|
|
65
|
-
# docs/README.md, the architecture index, the root README; ADR 0103 —
|
|
66
|
-
# converged each pass so a stale published copy is rewritten and the
|
|
67
|
-
# `reconcile`-is-a-no-op check guards them), plus every intake entry past
|
|
68
|
-
# its source.ttl (re-pull only when due, so external sources aren't
|
|
69
|
-
# hammered). Ttl-less intake entries (:no_policy) are skipped — they have
|
|
70
|
-
# no freshness contract and are never auto-re-pulled (ADR 0099). All are
|
|
71
|
-
# idempotent: publish writes only when the target's content changed.
|
|
72
|
-
def produce_scope(prefix, zone, file_stat)
|
|
73
|
-
publishable = @container.manifest.data.entries
|
|
74
|
-
.select { |e| e.derived? || !e.publish_tree.nil? || !e.publish_to.empty? }
|
|
75
|
-
.select { |e| in_scope?(e, prefix, zone) }.map(&:key)
|
|
76
|
-
stale_intake = Textus::Domain::Freshness::Evaluator.new(
|
|
77
|
-
manifest: @container.manifest, file_stat: file_stat, clock: Textus::Ports::Clock.new,
|
|
78
|
-
).stale_intake_keys(prefix: prefix, zone: zone)
|
|
79
|
-
(publishable + stale_intake).uniq
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def in_scope?(entry, prefix, zone)
|
|
83
|
-
return false if zone && entry.zone != zone
|
|
84
|
-
return false if prefix && !entry.key.start_with?(prefix)
|
|
85
|
-
|
|
86
|
-
true
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def dry_run_result(produce_keys, rows, health)
|
|
90
|
-
{
|
|
91
|
-
"protocol" => Textus::PROTOCOL, "ok" => true, "dry_run" => true,
|
|
92
|
-
"would_produce" => produce_keys,
|
|
93
|
-
"would_drop" => action_keys(rows, "drop"),
|
|
94
|
-
"would_archive" => action_keys(rows, "archive"),
|
|
95
|
-
"health" => health
|
|
96
|
-
}
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def apply_result(produced, swept, health)
|
|
100
|
-
{
|
|
101
|
-
"protocol" => Textus::PROTOCOL,
|
|
102
|
-
"ok" => produced[:failed].empty? && swept[:failed].empty?,
|
|
103
|
-
"dry_run" => false,
|
|
104
|
-
"produced" => produced[:produced],
|
|
105
|
-
"produce_failed" => produced[:failed],
|
|
106
|
-
"dropped" => swept[:dropped], "archived" => swept[:archived],
|
|
107
|
-
"failed" => swept[:failed],
|
|
108
|
-
"health" => health
|
|
109
|
-
}
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def action_keys(rows, action)
|
|
113
|
-
rows.select { |r| r["action"] == action }.map { |r| r["key"] }
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def publish_failed(failed)
|
|
117
|
-
@container.events.publish(
|
|
118
|
-
:reconcile_failed,
|
|
119
|
-
ctx: Textus::Hooks::Context.for(container: @container, call: @call),
|
|
120
|
-
failed: failed,
|
|
121
|
-
)
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
# Phase 2: destructive retention only (drop/archive). No refresh — intake
|
|
125
|
-
# re-pull is Produce's job (Phase 1). ADR 0093.
|
|
126
|
-
def apply(rows)
|
|
127
|
-
out = { dropped: [], archived: [], failed: [] }
|
|
128
|
-
delete = Write::KeyDelete.new(container: @container, call: @call)
|
|
129
|
-
rows.each do |row|
|
|
130
|
-
key = row["key"]
|
|
131
|
-
begin
|
|
132
|
-
case row["action"]
|
|
133
|
-
when "drop"
|
|
134
|
-
delete.call(key)
|
|
135
|
-
out[:dropped] << key
|
|
136
|
-
when "archive"
|
|
137
|
-
archive_leaf(row)
|
|
138
|
-
delete.call(key)
|
|
139
|
-
out[:archived] << key
|
|
140
|
-
end
|
|
141
|
-
rescue Textus::Error => e
|
|
142
|
-
out[:failed] << { "key" => key, "error" => e.message }
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
|
-
out
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
# Copy the leaf into <store>/archive/<relative-path> before deletion.
|
|
149
|
-
# (Lifted from the retired RetentionSweep#archive_leaf.)
|
|
150
|
-
def archive_leaf(row)
|
|
151
|
-
src = row["path"]
|
|
152
|
-
root = @container.root.to_s
|
|
153
|
-
rel = src.delete_prefix("#{root}/")
|
|
154
|
-
dest = File.join(root, "archive", rel)
|
|
155
|
-
FileUtils.mkdir_p(File.dirname(dest))
|
|
156
|
-
FileUtils.cp(src, dest)
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
end
|