file_hutch 0.2.0 → 0.3.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 +19 -0
- data/README.md +20 -0
- data/lib/file_hutch/backfill.rb +73 -0
- data/lib/file_hutch/client.rb +9 -0
- data/lib/file_hutch/rails/attachable.rb +0 -5
- data/lib/file_hutch/rails.rb +2 -1
- data/lib/file_hutch/version.rb +1 -1
- data/lib/tasks/file_hutch.rake +15 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4b59ec3bdd7aeb52b1e3b135698091716fcb3a38ac5ce686ec648631fe7b7087
|
|
4
|
+
data.tar.gz: 17485d436dbdcf4ddd85cd8c8c4706343281e13c433b0ba0544df81293ba0858
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c07c3d0cc5169acbb0ee860d8374be31128d62ba6fd9e03fbca8defed997b90313e706eb13e0f84f7dff5b2d12d0e6f036fd34c289eaa1f12f6f4523d7a8e088
|
|
7
|
+
data.tar.gz: e3b329321814427ffee8e657ae512e01201b9b5db1a505c64513f47f29e8f23e7db6d19671643ad7cdfb5632b5f1738f354274cdd351dc97530fdd6960d25f57
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 — 2026-09-24
|
|
4
|
+
|
|
5
|
+
### Removed
|
|
6
|
+
|
|
7
|
+
- `has_file_hutch_file`, deprecated in 0.2.0 as promised. Rename it to `has_hutch`; the options
|
|
8
|
+
are the same.
|
|
9
|
+
|
|
10
|
+
### Rails
|
|
11
|
+
|
|
12
|
+
- `bin/rails file_hutch:backfill MODEL=… FROM=…` moves an Active Storage attachment onto its
|
|
13
|
+
`has_hutch` column: streams each blob through FileHutch, or with `ADOPT_FROM=conn_…` registers
|
|
14
|
+
blobs in place when the Active Storage bucket is connected. Restartable; fills only blank rows.
|
|
15
|
+
Backed by `FileHutch::Backfill`.
|
|
16
|
+
|
|
17
|
+
### Client
|
|
18
|
+
|
|
19
|
+
- `adopt(key:, storage_connection:)` registers an object already in a connected bucket as a file,
|
|
20
|
+
without copying it. Idempotent: adopting the same key again returns the same file.
|
|
21
|
+
|
|
3
22
|
## 0.2.0 — 2026-09-16
|
|
4
23
|
|
|
5
24
|
### Rails
|
data/README.md
CHANGED
|
@@ -242,6 +242,26 @@ so a client cannot attach someone else's upload to the wrong field.
|
|
|
242
242
|
| `DirectUpload` JS | `file_hutch/direct_upload_controller` |
|
|
243
243
|
| service.yml, CORS, signed URL code | policies in the FileHutch dashboard |
|
|
244
244
|
|
|
245
|
+
Moving existing attachments across is one task. Add the column and `has_hutch`
|
|
246
|
+
beside the Active Storage attachment, then:
|
|
247
|
+
|
|
248
|
+
```sh
|
|
249
|
+
bin/rails file_hutch:backfill MODEL=User FROM=avatar
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Each blob is read through Active Storage and uploaded under the attachment's
|
|
253
|
+
policy, and its file ID goes into the column. Only rows whose column is still
|
|
254
|
+
blank are touched, so a run that stops half way (or leaves failures) carries on
|
|
255
|
+
where it was when you run it again. `TO=` names the `has_hutch` attachment when
|
|
256
|
+
it differs from the Active Storage one.
|
|
257
|
+
|
|
258
|
+
If the bucket Active Storage writes to is connected to your FileHutch project,
|
|
259
|
+
add `ADOPT_FROM=conn_…` and nothing is copied: each blob becomes a file where it
|
|
260
|
+
already is, by its key.
|
|
261
|
+
|
|
262
|
+
The Active Storage attachment is left alone. Read from the new column with a
|
|
263
|
+
fallback to the old one until the backfill has filled every row, then remove it.
|
|
264
|
+
|
|
245
265
|
## Development
|
|
246
266
|
|
|
247
267
|
```sh
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FileHutch
|
|
4
|
+
# Moves one Active Storage attachment on a model onto its has_hutch column:
|
|
5
|
+
#
|
|
6
|
+
# FileHutch::Backfill.new(model: User, from: :photo, to: :avatar).call
|
|
7
|
+
#
|
|
8
|
+
# Two ways, per run:
|
|
9
|
+
#
|
|
10
|
+
# * Stream (the default). Each blob's bytes are read through Active Storage
|
|
11
|
+
# and uploaded to FileHutch under the attachment's policy. Works whatever
|
|
12
|
+
# bucket Active Storage uses, and costs a download and an upload per file.
|
|
13
|
+
#
|
|
14
|
+
# * Adopt (adopt_from: "conn_…"). When the bucket Active Storage writes to is
|
|
15
|
+
# connected to FileHutch, each blob is registered where it already is, by
|
|
16
|
+
# its key. Nothing is copied or downloaded.
|
|
17
|
+
#
|
|
18
|
+
# Restartable. Only records whose column is still blank are touched, and the
|
|
19
|
+
# column is written as soon as each file exists, so a run that stops half way
|
|
20
|
+
# carries on from where it was. Adopting is idempotent on the server too.
|
|
21
|
+
#
|
|
22
|
+
# The Active Storage attachment is left alone: keep reading from it until the
|
|
23
|
+
# new column is filled everywhere, then remove it.
|
|
24
|
+
class Backfill
|
|
25
|
+
Result = Struct.new(:done, :skipped, :failed, keyword_init: true)
|
|
26
|
+
|
|
27
|
+
def initialize(model:, from:, to: from, adopt_from: nil, client: FileHutch.client, logger: FileHutch.config.logger)
|
|
28
|
+
@model, @from, @to, @adopt_from, @client, @logger = model, from.to_sym, to.to_sym, adopt_from.presence, client, logger
|
|
29
|
+
@options = model.file_hutch_files.fetch(@to) do
|
|
30
|
+
raise ArgumentError, "#{model.name} has no has_hutch :#{@to}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def call
|
|
35
|
+
result = Result.new(done: 0, skipped: 0, failed: 0)
|
|
36
|
+
|
|
37
|
+
@model.where(column => [ nil, "" ]).find_each do |record|
|
|
38
|
+
attachment = record.public_send(@from)
|
|
39
|
+
unless attachment.respond_to?(:attached?) && attachment.attached?
|
|
40
|
+
result.skipped += 1
|
|
41
|
+
next
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
file = @adopt_from ? adopt(attachment.blob) : stream(attachment.blob)
|
|
45
|
+
# update_column: the file exists already, so has_hutch's check on save
|
|
46
|
+
# would only ask the server again, and callbacks have no business here.
|
|
47
|
+
record.update_column(column, file.id)
|
|
48
|
+
result.done += 1
|
|
49
|
+
rescue FileHutch::Error => e
|
|
50
|
+
result.failed += 1
|
|
51
|
+
@logger&.warn("[file_hutch] backfill #{@model.name}##{record.id} #{@from}: #{e.message}")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
result
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def column = @options.fetch(:column)
|
|
60
|
+
def policy = @options.fetch(:policy)
|
|
61
|
+
|
|
62
|
+
def adopt(blob)
|
|
63
|
+
@client.adopt(storage_connection: @adopt_from, key: blob.key, filename: blob.filename.to_s,
|
|
64
|
+
content_type: blob.content_type, policy: policy)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def stream(blob)
|
|
68
|
+
blob.open do |io|
|
|
69
|
+
@client.upload(io, policy: policy, filename: blob.filename.to_s, content_type: blob.content_type)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/file_hutch/client.rb
CHANGED
|
@@ -35,6 +35,15 @@ module FileHutch
|
|
|
35
35
|
request(:get, "/api/v1/transforms").fetch("transforms").map { |t| Transform.new(t, client: self) }
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
# Registers an object that is already in a connected bucket as a file, in
|
|
39
|
+
# place: nothing is copied and the object keeps its key. Adopting the same
|
|
40
|
+
# key again returns the same file, so a backfill can be re-run.
|
|
41
|
+
def adopt(key:, storage_connection:, filename: nil, content_type: nil, visibility: nil, policy: nil)
|
|
42
|
+
body = { storage_connection: storage_connection, key: key, filename: filename,
|
|
43
|
+
content_type: content_type, visibility: visibility, policy: policy }.compact
|
|
44
|
+
File.new(request(:post, "/api/v1/files/adopt", body).fetch("file"), client: self)
|
|
45
|
+
end
|
|
46
|
+
|
|
38
47
|
# -- Declarative configuration (see `file_hutch plan|apply`) -----------
|
|
39
48
|
|
|
40
49
|
# The project as a config hash: {"uploads" => {...}, "transforms" => {...}, "environments" => [...]}.
|
|
@@ -45,11 +45,6 @@ module FileHutch
|
|
|
45
45
|
define_method(:"purge_#{name}") { file_hutch_purge(name) }
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
# The 0.1.0 name. Kept for one release so upgrading doesn't break a model.
|
|
49
|
-
def has_file_hutch_file(name, **options)
|
|
50
|
-
FileHutch.deprecator.warn("has_file_hutch_file is deprecated and will be removed in file_hutch 0.3. Use has_hutch :#{name} with the same options.")
|
|
51
|
-
has_hutch(name, **options)
|
|
52
|
-
end
|
|
53
48
|
|
|
54
49
|
def file_hutch_files
|
|
55
50
|
@file_hutch_files ||= superclass.respond_to?(:file_hutch_files) ? superclass.file_hutch_files.dup : {}
|
data/lib/file_hutch/rails.rb
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
require "rails"
|
|
4
4
|
require_relative "rails/attachable"
|
|
5
|
+
require_relative "backfill"
|
|
5
6
|
|
|
6
7
|
module FileHutch
|
|
7
|
-
def self.deprecator = @deprecator ||= ActiveSupport::Deprecation.new("0.
|
|
8
|
+
def self.deprecator = @deprecator ||= ActiveSupport::Deprecation.new("0.4", "file_hutch")
|
|
8
9
|
end
|
|
9
10
|
|
|
10
11
|
require_relative "rails/engine"
|
data/lib/file_hutch/version.rb
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :file_hutch do
|
|
4
|
+
desc "Copy an Active Storage attachment onto a has_hutch column. " \
|
|
5
|
+
"MODEL=User FROM=photo [TO=avatar] [ADOPT_FROM=conn_… to register blobs in place instead of copying]"
|
|
6
|
+
task backfill: :environment do
|
|
7
|
+
model = ENV.fetch("MODEL") { abort "MODEL= is required, e.g. MODEL=User" }.constantize
|
|
8
|
+
from = ENV.fetch("FROM") { abort "FROM= is required: the Active Storage attachment name" }
|
|
9
|
+
to = ENV.fetch("TO", from)
|
|
10
|
+
|
|
11
|
+
result = FileHutch::Backfill.new(model: model, from: from, to: to, adopt_from: ENV["ADOPT_FROM"]).call
|
|
12
|
+
puts "#{model.name}##{to}: #{result.done} filled, #{result.skipped} with nothing attached, #{result.failed} failed"
|
|
13
|
+
puts "Run it again to retry the failures; filled rows are skipped." if result.failed.positive?
|
|
14
|
+
end
|
|
15
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: file_hutch
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andy Leverenz
|
|
@@ -27,6 +27,7 @@ files:
|
|
|
27
27
|
- config/routes.rb
|
|
28
28
|
- exe/file_hutch
|
|
29
29
|
- lib/file_hutch.rb
|
|
30
|
+
- lib/file_hutch/backfill.rb
|
|
30
31
|
- lib/file_hutch/cli.rb
|
|
31
32
|
- lib/file_hutch/client.rb
|
|
32
33
|
- lib/file_hutch/configuration.rb
|
|
@@ -44,6 +45,7 @@ files:
|
|
|
44
45
|
- lib/generators/file_hutch/attachment/templates/migration.rb.tt
|
|
45
46
|
- lib/generators/file_hutch/install/install_generator.rb
|
|
46
47
|
- lib/generators/file_hutch/install/templates/initializer.rb
|
|
48
|
+
- lib/tasks/file_hutch.rake
|
|
47
49
|
homepage: https://github.com/filehutch/filehutch-ruby
|
|
48
50
|
licenses:
|
|
49
51
|
- MIT
|