Class: Aspera::TempFileManager

Inherits:
Object
  • Object
show all
Includes:
Singleton
Defined in:
lib/aspera/temp_file_manager.rb

Overview

create a temp file name for a given folder files can be deleted on process exit by calling cleanup

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize ⇒ TempFileManager

Returns a new instance of TempFileManager.



27
28
29
30
31
# File 'lib/aspera/temp_file_manager.rb', line 27

def initialize
  @created_files = []
  @cleanup_on_exit = true
  @global_temp = Etc.systmpdir
end

Instance Attribute Details

#cleanup_on_exit ⇒ Object

Returns the value of attribute cleanup_on_exit.



24
25
26
# File 'lib/aspera/temp_file_manager.rb', line 24

def cleanup_on_exit
  @cleanup_on_exit
end

#global_temp ⇒ Object

Returns the value of attribute global_temp.



25
26
27
# File 'lib/aspera/temp_file_manager.rb', line 25

def global_temp
  @global_temp
end

Class Method Details

.instance ⇒ TempFileManager

Returns the singleton instance of TempFileManager

Returns:



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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/aspera/temp_file_manager.rb', line 15

class TempFileManager
  include Singleton

  SEC_IN_DAY = 86_400
  # assume no transfer last longer than this
  # (garbage collect file list which were not deleted after transfer)
  FILE_LIST_AGE_MAX_SEC = SEC_IN_DAY * 5
  private_constant :SEC_IN_DAY, :FILE_LIST_AGE_MAX_SEC

  attr_accessor :cleanup_on_exit
  attr_reader :global_temp

  def initialize
    @created_files = []
    @cleanup_on_exit = true
    @global_temp = Etc.systmpdir
  end

  def global_temp=(value)
    @global_temp = case value
    when '@env' then Dir.tmpdir
    when '@sys' then Etc.systmpdir
    else value
    end
  end

  def delete_file(filepath)
    File.delete(filepath) if @cleanup_on_exit
  rescue => e
    Log.log.warn { "Problem deleting file: #{filepath}: #{e.message}" }
  end

  # Call this on process exit
  def cleanup
    @created_files.each do |filepath|
      delete_file(filepath) if File.file?(filepath)
    end
    @created_files = []
  end

  # Ensure that provided folder exists, or create it, atomically create and reserve a unique file.
  # The file is created immediately (O_EXCL) to prevent TOCTOU race conditions.
  # @return [String] path to that unique file
  def new_file_path_in_folder(temp_folder, prefix: nil, suffix: nil)
    FileUtils.mkdir_p(temp_folder)
    basename = [prefix || 'aspera', SecureRandom.uuid].join('-')
    # Create the file atomically with O_EXCL (retry with another name if it exists)
    # Note: Tempfile is not used, as its file is deleted when the object is garbage collected
    new_file = Dir::Tmpname.create([basename, suffix ? "-#{suffix}" : ''], temp_folder) do |path|
      File.open(path, File::WRONLY | File::CREAT | File::EXCL, 0o600) { nil }
    end
    @created_files.push(new_file)
    new_file
  end

  # Same as above but in global temp folder, with user's name
  def new_file_path_global(prefix = nil, suffix: nil)
    username =
      begin
        Etc.getlogin || Etc.getpwuid(Process.uid).name || 'unknown_user'
      rescue StandardError
        'unknown_user'
      end
    prefix = [prefix, username].compact.join('-')
    new_file_path_in_folder(@global_temp, prefix: prefix, suffix: suffix)
  end

  # Generate a unique directory path inside the global temp folder (does NOT create it).
  # Unlike new_file_path_global, the caller is expected to use the path as a directory.
  # @return [String] path to a unique (not yet existing) directory
  def new_dir_path_global(prefix = nil)
    username =
      begin
        Etc.getlogin || Etc.getpwuid(Process.uid).name || 'unknown_user'
      rescue StandardError
        'unknown_user'
      end
    dir_name = [prefix, username, SecureRandom.uuid].compact.join('-')
    File.join(@global_temp, dir_name)
  end

  # Garbage collect undeleted files
  def cleanup_expired(temp_folder)
    Dir.entries(temp_folder).each do |name|
      file_path = File.join(temp_folder, name)
      age_sec = (Time.now - File.stat(file_path).mtime).to_i
      # check age of file, delete too old
      if File.file?(file_path) && (age_sec > FILE_LIST_AGE_MAX_SEC)
        Log.log.debug { "garbage collecting #{name}" }
        delete_file(file_path)
      end
    end
  end
end

Instance Method Details

#cleanup ⇒ Object

Call this on process exit



48
49
50
51
52
53
# File 'lib/aspera/temp_file_manager.rb', line 48

def cleanup
  @created_files.each do |filepath|
    delete_file(filepath) if File.file?(filepath)
  end
  @created_files = []
end

#cleanup_expired(temp_folder) ⇒ Object

Garbage collect undeleted files



97
98
99
100
101
102
103
104
105
106
107
# File 'lib/aspera/temp_file_manager.rb', line 97

def cleanup_expired(temp_folder)
  Dir.entries(temp_folder).each do |name|
    file_path = File.join(temp_folder, name)
    age_sec = (Time.now - File.stat(file_path).mtime).to_i
    # check age of file, delete too old
    if File.file?(file_path) && (age_sec > FILE_LIST_AGE_MAX_SEC)
      Log.log.debug { "garbage collecting #{name}" }
      delete_file(file_path)
    end
  end
end

#delete_file(filepath) ⇒ Object



41
42
43
44
45
# File 'lib/aspera/temp_file_manager.rb', line 41

def delete_file(filepath)
  File.delete(filepath) if @cleanup_on_exit
rescue => e
  Log.log.warn { "Problem deleting file: #{filepath}: #{e.message}" }
end

#new_dir_path_global(prefix = nil) ⇒ String

Generate a unique directory path inside the global temp folder (does NOT create it). Unlike new_file_path_global, the caller is expected to use the path as a directory.

Returns:

  • (String) —

    path to a unique (not yet existing) directory



85
86
87
88
89
90
91
92
93
94
# File 'lib/aspera/temp_file_manager.rb', line 85

def new_dir_path_global(prefix = nil)
  username =
    begin
      Etc.getlogin || Etc.getpwuid(Process.uid).name || 'unknown_user'
    rescue StandardError
      'unknown_user'
    end
  dir_name = [prefix, username, SecureRandom.uuid].compact.join('-')
  File.join(@global_temp, dir_name)
end

#new_file_path_global(prefix = nil, suffix: nil) ⇒ Object

Same as above but in global temp folder, with user's name



71
72
73
74
75
76
77
78
79
80
# File 'lib/aspera/temp_file_manager.rb', line 71

def new_file_path_global(prefix = nil, suffix: nil)
  username =
    begin
      Etc.getlogin || Etc.getpwuid(Process.uid).name || 'unknown_user'
    rescue StandardError
      'unknown_user'
    end
  prefix = [prefix, username].compact.join('-')
  new_file_path_in_folder(@global_temp, prefix: prefix, suffix: suffix)
end

#new_file_path_in_folder(temp_folder, prefix: nil, suffix: nil) ⇒ String

Ensure that provided folder exists, or create it, atomically create and reserve a unique file. The file is created immediately (O_EXCL) to prevent TOCTOU race conditions.

Returns:

  • (String) —

    path to that unique file



58
59
60
61
62
63
64
65
66
67
68
# File 'lib/aspera/temp_file_manager.rb', line 58

def new_file_path_in_folder(temp_folder, prefix: nil, suffix: nil)
  FileUtils.mkdir_p(temp_folder)
  basename = [prefix || 'aspera', SecureRandom.uuid].join('-')
  # Create the file atomically with O_EXCL (retry with another name if it exists)
  # Note: Tempfile is not used, as its file is deleted when the object is garbage collected
  new_file = Dir::Tmpname.create([basename, suffix ? "-#{suffix}" : ''], temp_folder) do |path|
    File.open(path, File::WRONLY | File::CREAT | File::EXCL, 0o600) { nil }
  end
  @created_files.push(new_file)
  new_file
end