Class: Textus::Store::Mover

Inherits:
Object
  • Object
show all
Defined in:
lib/textus/store/mover.rb

Overview

rubocop:disable Metrics/AbcSize, Metrics/MethodLength

Instance Method Summary collapse

Constructor Details

#initialize(reader:, writer:, manifest:, audit_log:) ⇒ Mover

Returns a new instance of Mover.



7
8
9
10
11
12
# File 'lib/textus/store/mover.rb', line 7

def initialize(reader:, writer:, manifest:, audit_log:)
  @reader = reader
  @writer = writer
  @manifest = manifest
  @audit_log = audit_log
end

Instance Method Details

#call(old_key, new_key, as: Role::DEFAULT, dry_run: false) ⇒ Object

Raises:



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/textus/store/mover.rb', line 14

def call(old_key, new_key, as: Role::DEFAULT, dry_run: false)
  @manifest.validate_key!(old_key)
  @manifest.validate_key!(new_key)
  raise UsageError.new("mv: old and new keys are identical") if old_key == new_key

  old_mentry, old_path, = @manifest.resolve(old_key)
  raise UnknownKey.new(old_key) unless File.exist?(old_path)

  new_mentry, new_path, = @manifest.resolve(new_key)

  if old_mentry.zone != new_mentry.zone
    raise UsageError.new(
      "mv: cross-zone move refused (#{old_mentry.zone}#{new_mentry.zone}). " \
      "Use put+delete for cross-zone moves.",
    )
  end
  if old_mentry.format != new_mentry.format
    raise UsageError.new(
      "mv: format mismatch (#{old_mentry.format}#{new_mentry.format}); refusing.",
    )
  end

  writers = @manifest.zone_writers(old_mentry.zone)
  raise WriteForbidden.new(old_key, old_mentry.zone, writers: writers) unless writers.include?(as)

  raise UsageError.new("mv: target '#{new_key}' already exists at #{new_path}") if File.exist?(new_path)

  # Mint uid before the move so the audit row carries it.
  pre_env = @reader.get(old_key)
  current_uid = pre_env["uid"]
  etag_before = pre_env["etag"]

  if dry_run
    return {
      "protocol" => PROTOCOL, "ok" => true, "dry_run" => true,
      "from_key" => old_key, "to_key" => new_key,
      "from_path" => old_path, "to_path" => new_path,
      "uid" => current_uid
    }
  end

  if current_uid.nil?
    # Write the uid in place first so the source file carries it before mv.
    pre_env = @writer.put(old_key,
                          meta: pre_env["_meta"],
                          body: pre_env["body"],
                          content: pre_env["content"],
                          as: as,
                          suppress_events: true)
    current_uid = pre_env["uid"]
    etag_before = pre_env["etag"]
  end

  FileUtils.mkdir_p(File.dirname(new_path))
  FileUtils.mv(old_path, new_path)
  rewrite_name_for_mv!(new_mentry, new_path, new_key)
  etag_after = Etag.for_file(new_path)

  @audit_log.append(
    role: as, verb: "mv", key: new_key,
    etag_before: etag_before, etag_after: etag_after,
    extras: {
      "from_key" => old_key, "to_key" => new_key,
      "from_path" => old_path, "to_path" => new_path,
      "uid" => current_uid
    }
  )

  env = @reader.get(new_key)
  {
    "protocol" => PROTOCOL, "ok" => true,
    "from_key" => old_key, "to_key" => new_key,
    "from_path" => old_path, "to_path" => new_path,
    "uid" => current_uid,
    "envelope" => env
  }
end