Class: Secret::File

Inherits:
Object
  • Object
show all
Includes:
Encryption
Defined in:
lib/secret/file.rb

Overview

Handles file operations. Uses Ruby’s internal file locking mechanisms.

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Encryption

#change_encryption_passphrase!, #decrypt!, #decrypted, #encrypt!, #encrypted, #encrypted?, #ensure_unencrypted!, #remove_encrypted_indicator, #stash_encrypted

Constructor Details

#initialize(container, identifier) ⇒ File

Creates a new secret file. The specified identifier doesn’t already need to exist.

Parameters:

  • container (Secret::Container)

    the container object

  • identifier (Symbol)

    an unique identifier for this container

Raises:

  • (ArgumentError)


12
13
14
15
16
17
# File 'lib/secret/file.rb', line 12

def initialize(container, identifier)
  raise ArgumentError, "Container must be a Secret::Container object" unless container.is_a?(Secret::Container)
  @container = container; @identifier = identifier
  touch!
  ensure_writeable!
end

Instance Attribute Details

#containerObject (readonly)

Returns the value of attribute container.



7
8
9
# File 'lib/secret/file.rb', line 7

def container
  @container
end

#identifierObject (readonly)

Returns the value of attribute identifier.



7
8
9
# File 'lib/secret/file.rb', line 7

def identifier
  @identifier
end

Instance Method Details

#contentsString

Gets the contents of the file in a string format. Will return an empty string if the file doesn’t exist, or the file just so happens to be empty.

Returns:

  • (String)

    the contents of the file



62
63
64
65
66
67
68
# File 'lib/secret/file.rb', line 62

def contents
  str = nil
  stream 'r' do |f|
    str = f.read
  end
  return str
end

#delete!Object

Delete the contents of this file, along with any other associated files.



160
161
162
163
# File 'lib/secret/file.rb', line 160

def delete!
  ::File.delete(file_path) if exist?
  Dir[file_path + "*"].each {|f| ::File.delete f }
end

#ensure_writeable!Object



132
133
134
135
136
# File 'lib/secret/file.rb', line 132

def ensure_writeable!
  unless ::File.writable?(file_path)
    raise FileUnreadableError, "File is not writeable - perhaps it was created by a different process?"
  end
end

#exist?Boolean

Checks whether this file actually exists or not

Returns:

  • (Boolean)

    true if the file exists (i.e. has content), false if otherwise.



21
22
23
# File 'lib/secret/file.rb', line 21

def exist?
  ::File.exist?(file_path)
end

#restore_backup!Boolean

Attempts to restore a backup (i.e. if the computer crashed while doing a stash command)

Returns:

  • (Boolean)

    true if the backup was successfully restored, false otherwise



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/secret/file.rb', line 140

def restore_backup!
  return false unless ::File.exist?(backup_file_path)
  
  # We know backup exists, so let's write to the file. We want to truncate file contents.
  # Now copy file contents over from the backup file. We use this method to use locking.
  ::File.open(file_path, 'w', container.chmod_mode) do |f|
    begin
      f.flock ::File::LOCK_EX
      ::File.open(backup_file_path, 'r', container.chmod_mode) do |b|
        f.write b.read
      end
    ensure
      f.flock ::File::LOCK_UN
    end
  end
  return true
  
end

#secure!Object

Secures the file by chmoding it to 0700

Raises:

  • (IOError)

    if the file doesn’t exist on the server.



84
85
86
87
# File 'lib/secret/file.rb', line 84

def secure!
  raise IOError, "File doesn't exist" unless exist?
  ::File.chmod(container.chmod_mode, file_path)
end

#stash(content) ⇒ Object

Stashes some content into the file! This will write a temporary backup file before stashing, in order to prevent any partial writes if the server crashes. Once this finishes executing, you can be sure that contents have been written.

Parameters:

  • content (String)

    the contents to stash. **Must be a string!**

Raises:

  • (ArgumentError)

    if content is anything other than a String object!



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/secret/file.rb', line 95

def stash(content)
  raise ArgumentError, "Content must be a String (was type of type #{content.class.name})" unless content.is_a?(String)
  touch!
  ensure_writeable!
  
  # Think of this as a beginning of a transaction.
  ::File.open(file_path, 'a', container.chmod_mode) do |f|
    begin
      f.flock(::File::LOCK_EX)
  
      # Open a temporary file for writing, and close it immediately
      ::File.open(tmp_file_path, "w", container.chmod_mode){|f| f.write content }
  
      # Rename tmp file to backup file now we know contents are sane
      ::File.rename(tmp_file_path, backup_file_path)
      
      # Remove encryption indicator
      remove_encrypted_indicator
      
      # Truncate file contents to zero bytes
      f.truncate 0
      
      # Write content
      f.write content
    ensure
      # Now unlock file!
      f.flock(::File::LOCK_UN)
    end
    
    # Delete backup file
    ::File.delete(backup_file_path)
  end

  # Committed! Secure it just in case
  secure!
end

#stream(mode = 'r', &block) ⇒ IO

Note:

Uses an exclusive lock on this file

Gets a file stream of this file. If the file doesn’t exist, then a blank file will be created. By default, this allows you to write to the file. However, please use the #stash command, as it accounts for mid-write crashes. Don’t forget to close the file stream when you’re done!

Examples:

file = container.some_file

# Unsafe way!
io = file.stream
io.write "Hello World!"
io.close

# Safe way, with locking support
file.stream do |f|
  f.write "Hello World!"
end

Parameters:

  • mode (String) (defaults to: 'r')

    the mode for this file. Currently defaults to ‘r+’, which is read-write, with the file pointer at the beginning of the file.

Returns:

  • (IO)

    an IO stream to this file, if not using a block



44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/secret/file.rb', line 44

def stream(mode = 'r', &block)
  touch!
  ensure_writeable!
  return ::File.open(file_path, mode, container.chmod_mode)  unless block_given?
  ::File.open(file_path, mode, container.chmod_mode) do |f|
    begin
      f.flock(::File::LOCK_EX) # Lock with exclusive mode
      block.call(f)
    ensure
      f.flock(::File::LOCK_UN)
    end
  end
end

#touch!Boolean

Creates a new file if it doesn’t exist. Doesn’t actually change the last updated timestamp.

Returns:

  • (Boolean)

    true if an empty file was created, false if the file already existed.



73
74
75
76
77
78
79
80
# File 'lib/secret/file.rb', line 73

def touch!
  unless exist?
    ::File.open(file_path, 'w', container.chmod_mode) {}
    secure!
    return true
  end
  return false
end