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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 68acbe5c06713d98ad400877d375ccd63794b11ebe6e381aec9aea714061a0d6
4
- data.tar.gz: a9d3896f4d665648edce1641453a92abb12168802a67feae0f8c6d015d1d5fb5
3
+ metadata.gz: 4b59ec3bdd7aeb52b1e3b135698091716fcb3a38ac5ce686ec648631fe7b7087
4
+ data.tar.gz: 17485d436dbdcf4ddd85cd8c8c4706343281e13c433b0ba0544df81293ba0858
5
5
  SHA512:
6
- metadata.gz: 1ee3d96af2b6ab2b37d37036b466c4b6ed27ea40db2458c175e49723542464f595830786f228a02162cd65706c46dfac50429aa14213e080ceca1b3d2f78b0da
7
- data.tar.gz: 670315688e96fc2170634e086ab5c21871cfe38e516d8aef6a3b1b5212cc64e88088e7bb9d4846f2bca77b4eb1d82a3106929c372335235c5a3489e572450b8f
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
@@ -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 : {}
@@ -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.3", "file_hutch")
8
+ def self.deprecator = @deprecator ||= ActiveSupport::Deprecation.new("0.4", "file_hutch")
8
9
  end
9
10
 
10
11
  require_relative "rails/engine"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FileHutch
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -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.2.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