file_transactions 0.1.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bbdef21f6b3d981705c6a0ed618d90abbaee1e1f4d2e50f4c1233390a809584c
4
+ data.tar.gz: 0da72546f7cf4014ecaeb3101a169be1b7977b59d694deeaf3961c9ff0a21e1b
5
+ SHA512:
6
+ metadata.gz: 8dec98fc1b74aa2187e0ac76abd6b6b4a605327c5541e5aed0a9fbebf17783c4b4c0aae19bf3314f5aa3557eaded940ed59d9dd22d8d4722e966e22f1f7ca3d8
7
+ data.tar.gz: f8cbfc09aaee6af0fab0d3e783eb5f7d5a2818806cc657b1f40d639d618ac6ada0150b168558f4a989d4d52036f48710034ba26259e1006061a4baa0cbdd5365
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "file_transactions/version"
4
+ require 'file_transactions/error'
5
+ require 'file_transactions/base_command'
6
+ require 'file_transactions/change_file_command'
7
+ require 'file_transactions/create_directory_command'
8
+ require 'file_transactions/create_file_command'
9
+ require 'file_transactions/delete_file_command'
10
+ require 'file_transactions/move_file_command'
11
+ require 'file_transactions/transaction'
12
+
13
+ module FileTransactions
14
+ def self.transaction(&block)
15
+ Transaction.run(&block)
16
+ end
17
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FileTransactions
4
+ # A Base class that all commands must inherit from.
5
+ #
6
+ # This class provides all the necessary methods/hooks to make it possible to
7
+ # group commands together and/or nested inside transactions (and other
8
+ # commands).
9
+ class BaseCommand
10
+ def self.execute(*args, &block)
11
+ new(*args, &block).tap { |cmd| cmd.execute }
12
+ end
13
+
14
+ # Execute the command. This will trigger the following methods:
15
+ # * #before
16
+ # * #execute!
17
+ # * #after
18
+ def execute
19
+ scope = Transaction.scope
20
+ scope&.register self
21
+ prepare
22
+ run_before
23
+ run_excecute.tap { run_after }
24
+ rescue StandardError
25
+ self.failure_state = state
26
+ raise
27
+ ensure
28
+ Transaction.scope = scope
29
+ end
30
+
31
+ # Undo the changes made from a previous call to #execute. All previouly
32
+ # executed commands will be undone in reverse order.
33
+ def undo
34
+ raise Error, "Cannot undo #{self.class} which hasn't been executed" unless executed?
35
+
36
+ sub_commands[:after].reverse_each(&:undo)
37
+
38
+ ret = undo! unless failure_state == :before
39
+ sub_commands[:before].reverse_each(&:undo)
40
+ ret
41
+ end
42
+
43
+ # This registers a nested command. This method is called whever a command
44
+ # is executed and should not be called manually.
45
+ def register(command)
46
+ sub_commands[state] << command
47
+ end
48
+
49
+ # Returns true of false depending on if the commands has been executed.
50
+ def executed?
51
+ !!executed
52
+ end
53
+
54
+ # Returns true if the command has been unsuccessfully executed, otherwise false.
55
+ def failed?
56
+ !!failure_state
57
+ end
58
+
59
+ private
60
+
61
+ attr_accessor :state, :executed, :failure_state
62
+
63
+ def prepare
64
+ Transaction.scope = self
65
+ self.executed = true
66
+ end
67
+
68
+ def sub_commands
69
+ @sub_commands ||= {
70
+ before: [],
71
+ exec: [],
72
+ after: [],
73
+ }
74
+ end
75
+
76
+ def run_before
77
+ self.state = :before
78
+ before
79
+ end
80
+
81
+ def run_excecute
82
+ self.state = :exec
83
+ execute!
84
+ end
85
+
86
+ def run_after
87
+ self.state = :after
88
+ after
89
+ end
90
+
91
+ def before; end
92
+
93
+ def execute!
94
+ raise NotImplementedError, "#{self.clas} must implement #execute"
95
+ end
96
+
97
+ def undo!
98
+ raise NotImplementedError, "#{self.clas} must implement #undo"
99
+ end
100
+
101
+ def after; end
102
+ end
103
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'tmpdir'
5
+
6
+ module FileTransactions
7
+ class ChangeFileCommand < BaseCommand
8
+ attr_reader :name, :block
9
+
10
+ def initialize(name, &block)
11
+ @name = name
12
+ @block = block
13
+ end
14
+
15
+ private
16
+
17
+ def before
18
+ CreateFileCommand.execute(tmp_name) do
19
+ FileUtils.copy name, tmp_name
20
+ end
21
+ end
22
+
23
+ def execute!
24
+ value = block.call(name)
25
+ return unless value.is_a? String
26
+
27
+ File.open(name, 'w') { |f| f.write(value) }
28
+ end
29
+
30
+ def undo!
31
+ FileUtils.copy tmp_name, name
32
+ end
33
+
34
+ def tmp_name
35
+ @tmp_name ||= File.join(Dir.mktmpdir, File.basename(name))
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module FileTransactions
6
+ class CreateDirectoryCommand < BaseCommand
7
+ attr_reader :name
8
+
9
+ # Create new command for creating directories. N
10
+ #
11
+ # ==== Attributes
12
+ #
13
+ # * +name+ - The name of the directory to be created
14
+ #
15
+ # ==== Examples
16
+ #
17
+ # # Pass in the new directory name to ::new
18
+ # cmd1 = CreateDirectoryCommand.new('directory_name')
19
+ #
20
+ # # The new directory may be a path of multiple non exsting directories (like `mkdir -p`)
21
+ # cmd2 = CreateDirectoryCommand.new('non_existing_dir1/non_existing_dir2/new_dir')
22
+ def initialize(name)
23
+ @name = name
24
+ end
25
+
26
+ private
27
+
28
+ def execute!
29
+ dir = name
30
+
31
+ until Dir.exist? dir
32
+ directories.unshift dir
33
+ dir = File.dirname dir
34
+ end
35
+
36
+ directories.each { |dir| Dir.mkdir dir }
37
+ end
38
+
39
+ def undo!
40
+ directories.reverse_each { |dir| Dir.unlink dir }
41
+ end
42
+
43
+ def directories
44
+ @directories ||= []
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FileTransactions
4
+ class CreateFileCommand < BaseCommand
5
+ attr_reader :name, :block
6
+
7
+ def initialize(name, &block)
8
+ @name = name
9
+ @block = block
10
+ end
11
+
12
+ private
13
+
14
+ def before
15
+ dir = File.dirname(name)
16
+ return if Dir.exist? dir
17
+ CreateDirectoryCommand.execute(dir)
18
+ end
19
+
20
+ def execute!
21
+ value = block.call(name)
22
+ return unless value.is_a? String
23
+
24
+ File.open(name, 'w') { |f| f.write(value) }
25
+ end
26
+
27
+ def undo!
28
+ File.unlink name
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'tmpdir'
5
+
6
+ module FileTransactions
7
+ class DeleteFileCommand < BaseCommand
8
+ attr_reader :name, :block
9
+
10
+ def initialize(name)
11
+ @name = name
12
+ end
13
+
14
+ private
15
+
16
+ def before
17
+ CreateFileCommand.execute(tmp_name) do
18
+ FileUtils.copy name, tmp_name
19
+ end
20
+ end
21
+
22
+ def execute!
23
+ File.delete name
24
+ end
25
+
26
+ def undo!
27
+ FileUtils.copy tmp_name, name
28
+ end
29
+
30
+ def tmp_name
31
+ @tmp_name ||= File.join(Dir.mktmpdir, File.basename(name))
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FileTransactions
4
+ class Error < StandardError; end
5
+ class Rollback < Error; end
6
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FileTransactions
4
+ class MoveFileCommand < BaseCommand
5
+ attr_reader :from, :to
6
+
7
+ def initialize(from:, to:)
8
+ @from = from
9
+ @to = to
10
+ end
11
+
12
+ private
13
+
14
+ def before
15
+ CreateDirectoryCommand.execute(File.dirname(to))
16
+ end
17
+
18
+ def execute!
19
+ File.rename from, to
20
+ end
21
+
22
+ def undo!
23
+ File.rename to, from
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FileTransactions
4
+ class Transaction
5
+ class << self
6
+ def run(&block)
7
+ new(&block).__send__(:run)
8
+ end
9
+
10
+ def scope=(scope)
11
+ Thread.current['FT.scope'] = scope
12
+ end
13
+
14
+ def scope
15
+ Thread.current['FT.scope']
16
+ end
17
+
18
+ end
19
+
20
+ def initialize(&block)
21
+ raise Error, 'A block must be given' unless block_given?
22
+
23
+ @block = block
24
+ @commands = []
25
+ end
26
+
27
+ def register(command)
28
+ commands << command
29
+ end
30
+
31
+ def rollback
32
+ return if backrolled?
33
+
34
+ commands.reverse_each(&:undo)
35
+ self.backrolled = true
36
+ end
37
+ alias undo rollback
38
+
39
+ def backrolled?
40
+ !!backrolled
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :block, :commands
46
+ attr_accessor :backrolled
47
+
48
+ def run
49
+ scope = Transaction.scope
50
+ scope&.register self
51
+ Transaction.scope = self
52
+ block.call
53
+ rescue StandardError => e
54
+ rollback
55
+ raise unless Rollback === e
56
+ ensure
57
+ Transaction.scope = scope
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FileTransactions
4
+ VERSION = "0.1.0"
5
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: file_transactions
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sammy Henningsson
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-06-18 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |
14
+ A set of file operation that can be undone or wrapped in a
15
+ transaction. If the transaction is rolled back then all file
16
+ operations will be undone.
17
+ email:
18
+ - sammy.henningsson@gmail.com
19
+ executables: []
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - lib/file_transactions.rb
24
+ - lib/file_transactions/base_command.rb
25
+ - lib/file_transactions/change_file_command.rb
26
+ - lib/file_transactions/create_directory_command.rb
27
+ - lib/file_transactions/create_file_command.rb
28
+ - lib/file_transactions/delete_file_command.rb
29
+ - lib/file_transactions/error.rb
30
+ - lib/file_transactions/move_file_command.rb
31
+ - lib/file_transactions/transaction.rb
32
+ - lib/file_transactions/version.rb
33
+ homepage: https://github.com/sammyhenningsson/file_transactions
34
+ licenses:
35
+ - MIT
36
+ metadata:
37
+ homepage_uri: https://github.com/sammyhenningsson/file_transactions
38
+ post_install_message:
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '2.5'
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ requirements: []
53
+ rubygems_version: 3.0.3
54
+ signing_key:
55
+ specification_version: 4
56
+ summary: Transactions for file operations
57
+ test_files: []