| 
									
										
										
										
											2024-01-11 15:13:12 -08:00
										 |  |  | # typed: strict | 
					
						
							| 
									
										
										
										
											2023-11-29 15:18:14 +00:00
										 |  |  | # frozen_string_literal: true | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | require "fileutils" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class File | 
					
						
							|  |  |  |   # Write to a file atomically. Useful for situations where you don't | 
					
						
							|  |  |  |   # want other processes or threads to see half-written files. | 
					
						
							|  |  |  |   # | 
					
						
							|  |  |  |   #   File.atomic_write('important.file') do |file| | 
					
						
							|  |  |  |   #     file.write('hello') | 
					
						
							|  |  |  |   #   end | 
					
						
							|  |  |  |   # | 
					
						
							|  |  |  |   # This method needs to create a temporary file. By default it will create it | 
					
						
							|  |  |  |   # in the same directory as the destination file. If you don't like this | 
					
						
							|  |  |  |   # behavior you can provide a different directory but it must be on the | 
					
						
							|  |  |  |   # same physical filesystem as the file you're trying to write. | 
					
						
							|  |  |  |   # | 
					
						
							|  |  |  |   #   File.atomic_write('/data/something.important', '/data/tmp') do |file| | 
					
						
							|  |  |  |   #     file.write('hello') | 
					
						
							|  |  |  |   #   end | 
					
						
							| 
									
										
										
										
											2024-01-11 15:13:12 -08:00
										 |  |  |   sig { | 
					
						
							| 
									
										
										
										
											2025-07-14 14:48:08 +01:00
										 |  |  |     type_parameters(:Out).params( | 
					
						
							| 
									
										
										
										
											2024-01-11 15:13:12 -08:00
										 |  |  |       file_name: T.any(Pathname, String), | 
					
						
							|  |  |  |       temp_dir:  String, | 
					
						
							| 
									
										
										
										
											2025-07-14 14:48:08 +01:00
										 |  |  |       _block:    T.proc.params(arg0: Tempfile).returns(T.type_parameter(:Out)), | 
					
						
							|  |  |  |     ).returns(T.type_parameter(:Out)) | 
					
						
							| 
									
										
										
										
											2024-01-11 15:13:12 -08:00
										 |  |  |   } | 
					
						
							|  |  |  |   def self.atomic_write(file_name, temp_dir = dirname(file_name), &_block) | 
					
						
							| 
									
										
										
										
											2023-11-29 15:18:14 +00:00
										 |  |  |     require "tempfile" unless defined?(Tempfile) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     Tempfile.open(".#{basename(file_name)}", temp_dir) do |temp_file| | 
					
						
							|  |  |  |       temp_file.binmode | 
					
						
							|  |  |  |       return_val = yield temp_file | 
					
						
							|  |  |  |       temp_file.close | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       old_stat = if exist?(file_name) | 
					
						
							|  |  |  |         # Get original file permissions | 
					
						
							|  |  |  |         stat(file_name) | 
					
						
							|  |  |  |       else | 
					
						
							|  |  |  |         # If not possible, probe which are the default permissions in the | 
					
						
							|  |  |  |         # destination directory. | 
					
						
							|  |  |  |         probe_stat_in(dirname(file_name)) | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if old_stat | 
					
						
							|  |  |  |         # Set correct permissions on new file | 
					
						
							|  |  |  |         begin | 
					
						
							| 
									
										
										
										
											2024-01-11 15:13:12 -08:00
										 |  |  |           chown(old_stat.uid, old_stat.gid, T.must(temp_file.path)) | 
					
						
							| 
									
										
										
										
											2023-11-29 15:18:14 +00:00
										 |  |  |           # This operation will affect filesystem ACL's | 
					
						
							| 
									
										
										
										
											2024-01-11 15:13:12 -08:00
										 |  |  |           chmod(old_stat.mode, T.must(temp_file.path)) | 
					
						
							| 
									
										
										
										
											2023-11-29 15:18:14 +00:00
										 |  |  |         rescue Errno::EPERM, Errno::EACCES | 
					
						
							|  |  |  |           # Changing file ownership failed, moving on. | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       # Overwrite original file with temp file | 
					
						
							| 
									
										
										
										
											2024-01-11 15:13:12 -08:00
										 |  |  |       rename(T.must(temp_file.path), file_name) | 
					
						
							| 
									
										
										
										
											2023-11-29 15:18:14 +00:00
										 |  |  |       return_val | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Private utility method. | 
					
						
							| 
									
										
										
										
											2024-01-11 15:13:12 -08:00
										 |  |  |   sig { params(dir: String).returns(T.nilable(File::Stat)) } | 
					
						
							|  |  |  |   private_class_method def self.probe_stat_in(dir) # :nodoc: | 
					
						
							| 
									
										
										
										
											2023-11-29 15:18:14 +00:00
										 |  |  |     basename = [ | 
					
						
							|  |  |  |       ".permissions_check", | 
					
						
							|  |  |  |       Thread.current.object_id, | 
					
						
							|  |  |  |       Process.pid, | 
					
						
							| 
									
										
										
										
											2024-01-11 15:13:12 -08:00
										 |  |  |       rand(1_000_000), | 
					
						
							| 
									
										
										
										
											2023-11-29 15:18:14 +00:00
										 |  |  |     ].join(".") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     file_name = join(dir, basename) | 
					
						
							|  |  |  |     FileUtils.touch(file_name) | 
					
						
							|  |  |  |     stat(file_name) | 
					
						
							| 
									
										
										
										
											2024-01-11 15:13:12 -08:00
										 |  |  |   rescue Errno::ENOENT | 
					
						
							|  |  |  |     file_name = nil | 
					
						
							| 
									
										
										
										
											2023-11-29 15:18:14 +00:00
										 |  |  |   ensure | 
					
						
							|  |  |  |     FileUtils.rm_f(file_name) if file_name | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | end |