Class: Secret::File

Inherits:
Object
  • Object
show all
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

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)

include Secret::Encryption



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

def container
  @container
end

#identifierObject (readonly)

include Secret::Encryption



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

#ensure_writeable!Object



129
130
131
132
133
# File 'lib/secret/file.rb', line 129

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



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

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
# 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)
      
      # 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