Skip to content

Instantly share code, notes, and snippets.

@coderanger
Created May 18, 2016 00:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save coderanger/fcb8c29cf3331c43c3f12c1aa465645d to your computer and use it in GitHub Desktop.
Save coderanger/fcb8c29cf3331c43c3f12c1aa465645d to your computer and use it in GitHub Desktop.
File transport API sketch.
# Remote file or directory proxy object.
#
# @since 1.0.0
class File
# Create a proxy object. All data is lazy-loaded so this does very little.
#
# @param connection [Airlift::Connection] Connection object to use for
# operations.
# @param path [String] File or directory path.
# @param follow_symlink [Boolean] Follow symlinks when getting file info.
def initialize(connection, path, follow_symlink: true)
@connection = connection
@path = path
@follow_symlink = follow_symlink
end
# @!group Accessors
# =================
# @attribute connection
# Connection object to use for operations.
# @return [Airlift::Connection]
attr_reader :connection
# @attribute path
# File or directory path.
# @return [String]
attr_reader :path
# @attribute follow_symlink
# Follow symlinks when getting file info.
# @return [Boolean]
attr_reader :follow_symlink
# @!group Core API
# ================
# Load file content in to a string. Returns nil if the file does not exist
# or is not readable.
#
# @return [String, nil]
def content
return @content if defined?(@content)
buf = ''
if @connection.download_file(@path) {|data| buf << data }
@content = buf
else
# File wasn't readable.
@content = nil
end
end
# Upload new file content from a string.
#
# @param data [String] New file content.
# @return [void]
def content=(data)
@content = data
@connection.upload_file(@path) {|send| send.call(data) }
end
# Gather file metadata information. Returns nil if the file does not exist
# or is not readable.
#
# @return [Airlift::Stat, nil]
def stat
return @stat if defined?(@stat)
@stat = @connection.stat_file(@path, follow_symlink: @follow_symlink)
end
# Download to a local path. Returns true if the file was downloaded and
# false if the file does not exist or is not readable.
#
# @param local_path [String] Path to download to.
# @return [Boolean]
def download(local_path)
local_file = ::File.open(local_path, 'wb')
@connection.download_file(@path) {|data| local_file.write(data) }
end
# Upload from a local file. Returns true if the file was uploaded and false
# if the file could not be written.
#
# @param local_path [String] Path to upload from.
# @return [Boolean]
def upload(local_path)
@connection.upload_file(@path) do |send|
local_file = ::File.open(local_path, 'rb')
while data = local_file.read(1024)
send.call(data)
end
end
end
# Synchronize a local and remote directory. Any new files in the local path
# will be uploaded and extra files on the remote side will be removed.
# Returns true if the sync succeeds and false if it fails.
#
# @param local_path [String] Path to sync from.
# @return [Boolean]
def sync(local_path)
@connect.sync_files(local_path, @path)
end
# @!group Exception API
# =====================
# Load file content or raise an exception.
#
# @see #content
def content!
content.tap do |data|
raise Error::FileError.new("Unable to get content for #{@path}") if data.nil?
end
end
# TODO come up with a good name for this. content!= is not a valid method name.
#def content!=
#end
# Gather file information or raise an exception.
#
# @see #stat
def stat!
stat.tap do |data|
raise Error::FileError.new("Unable to stat #{@path}") if data.nil?
end
end
# Download file or raise an exception.
#
# @see #download
def download!(local_path)
download(local_path).tap do |success|
raise Error::FileError.new("Unable to download #{@path}") unless success
end
end
# Upload file or raise an exception.
#
# @see #upload
def upload!(local_path)
upload(local_path).tap do |success|
raise Error::FileError.new("Unable to upload #{@path}") unless success
end
end
# Synchronize a local and remote directory or raise an exception.
#
# @see #sync
def sync!(local_path)
stat(local_path).tap do |success|
raise Error::FileError.new("Unable to sync #{@path}") unless success
end
end
# @!group Sugar Helpers
# =====================
# Return the proxy corresponding to the unfollowed symlink source for the
# same path.
#
# @return [Airlift::File]
def link_source
if @follow_symlink
self.class.new(@connection, @path, follow_symlink: false)
else
self
end
end
# Check if this is a normal file.
#
# @return [Boolean]
def file?
stat && stat[:type] == :file
end
# Check if this is a directory.
#
# @return [Boolean]
def directory?
stat && stat[:type] == :directory
end
# Check if this is a symlink.
#
# @return [Boolean]
def symlink?
source_stat = source.stat
source_stat && source_stat[:type] == :symlink
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment