file_transactions 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []