Skip to content

Instantly share code, notes, and snippets.

@calebastey
Created September 30, 2019 20:22
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 calebastey/130f38bd7b2bde97e4efcdfa178ad38c to your computer and use it in GitHub Desktop.
Save calebastey/130f38bd7b2bde97e4efcdfa178ad38c to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby -w
require 'cgi'
require 'io/console'
require 'json'
require 'logger'
require 'net/http'
require 'optionparser'
require 'pathname'
require 'uri'
class FilesDotCom
# Struct to hold the configuration options read in from the command line
Options = Struct.new(
:api_key,
:district,
:username,
:full_name,
:email,
:user_type,
:public_key_path,
:pm_username,
:analyst_usernames,
:pd_usernames,
:verbose,
:dry_run
)
end
# Class for managing files.com resources and configurations via the the [https://developers.files.com](API)
class FilesDotCom::Manager < FilesDotCom
# Dummy class to serve as our Net::HTTP instance in dry run mode
class DryRunHttp
def request(request)
end
end
LOGGER = Logger.new(STDOUT)
LOGGER.level = Logger::INFO
API_BASE = "https://app.files.com/api/rest/v1"
API_BASE_URI = URI(API_BASE)
POST = 'POST'
GET = 'GET'
PUT = 'PUT'
USER_ID_CACHE = {}
GROUP_ID_CACHE = {}
DIRECTORY_ID_CACHE = {}
# stupidly large int to be used as a dummy value for IDs in dry run mode so we don't damage anything if we accidentally
# make a real API call
DRY_RUN_ID = 4611686018427387903
attr_accessor :http, :options, :dry_run
# Initialize a `FilesDotComManager`
#
# @param api_key [String] the API key to be used for files.com authentication
# @return verbose [Boolean] whether to enable verbose (`DEBUG`-level) logging
def initialize(api_key:, verbose: false, dry_run: true)
@api_key = api_key
LOGGER.level = Logger::DEBUG if verbose
@dry_run = dry_run
if dry_run
@http = DryRunHttp.new
original_formatter = Logger::Formatter.new
LOGGER.formatter = proc { |severity, datetime, progname, msg|
original_formatter.call(severity, datetime, progname, "[DRY RUN] #{msg}\n")
}
LOGGER.info "Running in dry-run mode. Requests will be logged but nothing will actually happen."
else
@http = Net::HTTP.start(API_BASE_URI.host, API_BASE_URI.port, :use_ssl => API_BASE_URI.scheme == 'https')
end
end
# Make an HTTP request against the an endpoint on the files.com API
#
# @param endpoint [String] the full endpoint path including any query parameters
# @param method [String] the HTTP method to be used. Must be one of [`GET`, `PUT`, `POST`]
# @param authenticate [Boolean] whether to authenticate the request with the API token; defaults to `true`
#
# @return [Net::HTTPResponse] the response returned from the request
# @throws IOError if there is an error making the request or a non-2xx status is returned
def http_request(endpoint:, method: GET, data: {}, authenticate: true)
uri = URI("#{API_BASE}/#{endpoint}")
case method
when GET
request = Net::HTTP::Get.new uri
when POST
request = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json')
request.body = data.to_json
when PUT
request = Net::HTTP::Put.new(uri, 'Content-Type' => 'application/json')
request.body = data.to_json
else
raise ArgumentError, "Invalid request method: #{method}"
end
request.basic_auth @api_key, "xxx" if authenticate
request['Accept'] = 'application/json'
LOGGER.debug "Making #{method} request to URI #{uri} with data #{data}"
return if @dry_run
response = request_with_retries(request: request)
raise IOError, "Received response code #{response[:data].code} with body #{response[:data].body}" unless response[:success]
LOGGER.debug "Received response code #{response[:data].code} with body #{response[:data].body}"
response[:data]
end
# Makes a request with retry logic. Files.com will occasionally throw 500s, stopping the entire job. This is a
# known bug and the retry logic should provide a bit of a workaround.
#
# @param request HTTP request to retry
# @param num_retries Number of times to try for success before giving up
#
# @return a map containing whether or not it was successful and the last response before success or giving up
def request_with_retries(request:, num_retries: 5)
tries = 0
success = false
while tries < num_retries and !success do
response = @http.request request
if response.code.to_i < 300
success = true
else
LOGGER.info "Received response code #{response.code} with body #{response.body}. Retrying #{num_retries - tries} times"
end
tries += 1
end
{:success => success, :data => response}
end
# Create a directory on files.com. Noop if the directory already exists
#
# @param directory_name [String] the full path to the directory to be created
#
# @return [Net::HTTPResponse] the response from the directory creation request
# @throws IOError if there is an error making the request or a non-2xx status is returned
def create_directory(directory_name:)
id = get_directory_id directory_name
unless id.nil?
LOGGER.info "Not creating directory #{directory_name} because it already exists"
return id
end
LOGGER.info "Creating directory #{directory_name}"
endpoint = "folders/#{directory_name}"
http_request(endpoint: endpoint, method: POST)
end
# Create a files.com user. Noop if the user already exists
#
# @param username [String] the username
# @param email [String] the email associated with the user
# @param full_name [String] the user's full name
# @param user_type [String] the type of user to be created; must be one of :uploader, :viewer, :uploader_viewer, :bot]
# Create a directory on files.com. Noop if the directory already exists
#
# @return [Net::HTTPResponse] the response from the user creation request
# @throws IOError if there is an error making the request or a non-2xx status is returned
def create_user(username:, email:, full_name:, user_type:)
id = get_user_id username
unless id.nil?
LOGGER.info "Not creating user #{username} because it already exists"
return id
end
LOGGER.info "Creating user #{username}"
is_bot_user = user_type == :bot
user_data = {
email: email,
authentication_method: "password",
dav_permission: false,
ftp_permission: false,
language: "en",
name: full_name,
receive_admin_alerts: false,
require_password_change: true,
restapi_permission: !is_bot_user, # this covers both API and web UI access
self_managed: !is_bot_user,
sftp_permission: true,
site_admin: false,
ssl_required: "always_require",
time_zone: "Pacific Time (US & Canada)",
username: username,
}
LOGGER.debug("Creating user with attributes #{user_data}")
endpoint = "users.json"
response = http_request(endpoint: endpoint, method: POST, data: user_data)
unless @dry_run
id = JSON.parse(response.body)['id']
else
id = DRY_RUN_ID
end
USER_ID_CACHE[username] = id
if user_type != "bot"
generate_password_reset(username: username)
end
id
end
# Create a group on files.com. Noop if the group already exists
#
# @param group_name [String] the name of the group to be created
#
# @return [Net::HTTPResponse] the response from the group creation request
# @throws IOError if there is an error making the request or a non-2xx status is returned
def create_group(group_name:)
id = get_group_id group_name
unless id.nil?
LOGGER.info "Not creating group #{group_name} becausae it already exists"
return id
end
LOGGER.info "Creating group #{group_name}"
endpoint = "groups.json"
data = { name: group_name }
response = http_request(endpoint: endpoint, method: POST, data: data)
unless @dry_run
id = JSON.parse(response.body)['id']
else
id = DRY_RUN_ID
end
GROUP_ID_CACHE[group_name] = id
id
end
# Add a permission on a folder for a group on files.com. Noop if the permission already exists
#
# @param group_name [String] the name of the group for which the permission is being added
# @param directory_name [String] the name of the directory on which the permission is being added
# @param permission_type [String] the type of permission to add. Must be one of :full, :readonly, :writeonly, :previewonly,
# :history]
#
# @return [Net::HTTPResponse] the response from the permission creation request
# @throws IOError if there is an error making the request or a non-2xx status is returned
def add_permission_to_group(group_name:, directory_name:, permission_type:)
if group_has_permission?(
group_name: group_name,
directory_name: directory_name,
permission_type: permission_type
)
LOGGER.info "Not adding #{permission_type} permission on directory #{directory_name} to group #{group_name} because it already exists"
return
end
LOGGER.info "Adding #{permission_type} permission on directory #{directory_name} to group #{group_name}"
endpoint = "permissions.json"
unless @dry_run
group_id = get_group_id group_name
else
group_id = DRY_RUN_ID
end
if group_id.nil?
LOGGER.info "Not adding #{permission_type} permission on directory #{directory_name} to group #{group_name} because the group does not exist"
return
end
data = {
group_id: group_id,
path: directory_name,
recursive: true,
permission: permission_type
}
http_request(endpoint: endpoint, method: POST, data: data)
end
# Add a notification for files uploaded to a given folder on files.com. Noop if the notification already exists
#
# @param directory_name [String] the name of the directory on which the notification is being added
# @param group_name [String] the name of the group for which the notification is being added
#
# @return [Net::HTTPResponse] the response from the notification creation request
# @throws IOError if there is an error making the request or a non-2xx status is returned
def add_notification_to_directory(directory_name:, group_name:)
if group_has_notification?(group_name: group_name, directory_name: directory_name)
LOGGER.info "Not adding notification for group #{group_name} on directory #{directory_name} because it already exists"
return
end
LOGGER.info "Adding notification for group #{group_name} on directory #{directory_name}"
unless @dry_run
group_id = get_group_id group_name
else
group_id = DRY_RUN_ID
end
if group_id.nil?
LOGGER.info "Not adding notification for group #{group_name} on directory #{directory_name} because the group does not exist"
return
end
endpoint = "notifications.json"
data = {
group_id: group_id,
notify_on_copy: true,
path: directory_name,
send_interval: "five_minutes"
}
http_request(endpoint: endpoint, method: POST, data: data)
end
# Add a user to a group on files.com. Request is idempotent.
#
# @param username [String] the name of the user to be added to th egroup
# @param group_name [String] the name of the group for which the user is being added
#
# @return [Net::HTTPResponse] the response from the group addition request
# @throws IOError if there is an error making the request or a non-2xx status is returned
def add_user_to_group(username:, group_name:)
LOGGER.info "Adding user #{username} to group #{group_name}"
unless @dry_run
group_id = get_group_id group_name
user_id = get_user_id username
else
group_id = DRY_RUN_ID
user_id = DRY_RUN_ID
end
if group_id.nil? || user_id.nil?
LOGGER.info "Not adding user #{username} to group #{group_name} because the user or group does not exist"
end
endpoint = "groups/#{group_id}/memberships/#{user_id}.json"
data = { user_id: user_id, admin: false }
http_request(endpoint: endpoint, method: PUT, data: data)
end
# Add a public key for a user on files.com. Request is idempotent
#
# @param username [String] the username to which the key is being added
# @param public_key_path [String] the path to the public key on the file system. Must be a valid path. May be absolute or
# relative to the script directory
#
# @return [Net::HTTPResponse] the response from the key addition request
# @throws IOError if there is an error making the request or a non-2xx status is returned
def add_public_key(username:, public_key_path:)
LOGGER.info "Adding public key for user #{username} from path #{public_key_path}}"
unless @dry_run
user_id = get_user_id(username)
else
user_id = DRY_RUN_ID
end
raise ArgumentError, "Public key file #{public_key_path} does not exist" unless File.exist? public_key_path
endpoint = "users/#{user_id}/public_keys"
data = {
title: "Public SSH Key",
public_key: File.open(public_key_path, 'r').read
}
http_request(endpoint: endpoint, method: POST, data: data)
end
# Trigger a password reset and generate a password reset email for a user on files.com
#
# @param username [String] the user whose password is being reset
#
# @return [Net::HTTPResponse] the response from the password reset request
# @throws IOError if there is an error making the request or a non-2xx status is returned
def generate_password_reset(username:)
LOGGER.info("Generating password reset email for user #{username}")
endpoint = "sessions/forgot"
data = {username: username}
http_request(endpoint: endpoint, method: POST, data: data, authenticate: false)
end
# Check whether a group on files.com is set to receive notifications for files added to a given directory
#
# @param directory_name [String] the name of the directory on which to check for notifications
# @param group_name [String] the name of the group to check for notifications on the directory
#
# @return [Boolean] `true` if the group is set to receive notifications on the specified folder
# @throws IOError if there is an error making the request or a non-2xx status is returned
def group_has_notification?(group_name:, directory_name:)
LOGGER.info "Checking if group #{group_name} has notifications on directory #{directory_name}"
unless @dry_run
group_id = get_group_id(group_name)
else
group_id = DRY_RUN_ID
end
return false if group_id.nil?
endpoint = "notifications"
response = http_request(endpoint: endpoint, method: GET)
return false if @dry_run
data = JSON.parse(response.body).select do |notification|
notification['path'] == directory_name && notification['group_id'] == group_id
end
return !data.empty?
end
# Check whether a group on files.com has a given permission type on a given directory
#
# @param directory_name [String] the name of the directory on which to check for permissions
# @param group_name [String] the name of the group to check for permissions on the directory
# @param permission_type [String] the type of permission to check for. Must be one of [`full, `readonly`, `writeonly`,
# `previewonly`, `history`]
#
# @return [Boolean] `true` if the group has the specified permission on the specified folder
# @throws IOError if there is an error making the request or a non-2xx status is returned
def group_has_permission?(group_name:, directory_name:, permission_type:)
LOGGER.info "Checking if group #{group_name} has permission #{permission_type} on directory #{directory_name}"
unless @dry_run
group_id = get_group_id(group_name)
else
group_id = DRY_RUN_ID
end
return false if group_id.nil?
endpoint = "groups/#{group_id}/permissions"
response = http_request(endpoint: endpoint, method: GET)
return false if @dry_run
data = JSON.parse(response.body).select do |permission|
permission['path'] == directory_name && permission['permission'] == permission_type
end
return !data.empty?
end
# Get the ID for a given directory on files.com
#
# @param directory_name [String] the name of the directory
#
# @return [Integer] the ID for the directory or `nil` if the directory does not exist
# @throws IOError if there is an error making the request or a non-2xx status is returned
def get_directory_id(directory_name)
absolute_path = directory_name.gsub(%r{^([^/])}, '/\1')
LOGGER.info "Getting ID for directory #{absolute_path}"
id = DIRECTORY_ID_CACHE[directory_name]
return id unless id.nil?
pathname = Pathname(absolute_path)
parent = pathname.parent.to_s
directory = pathname.basename.to_s
endpoint = "folders/#{parent}?filter=#{directory}"
response = http_request(endpoint: endpoint, method: GET)
return nil if @dry_run
data = JSON.parse(response.body)
return nil if data.empty?
if (data.length > 1)
raise NameError, "Did not find exactly one result for directory #{directory_name}. Found #{data.length} results"
end
id = data[0]['id']
DIRECTORY_ID_CACHE[directory_name] = id
id
end
# Get the ID for a given user on files.com
#
# @param directory_name [String] the username
#
# @return [Integer] the ID for the user or `nil` if the user does not exist
# @throws IOError if there is an error making the request or a non-2xx status is returned
def get_user_id(username)
LOGGER.info "Getting ID for user #{username}"
id = USER_ID_CACHE[username]
return id unless id.nil?
# URL-escape the username in the query to ensure the correct results are returned if special characters are present
endpoint = "users?q[username]=#{CGI.escape username}"
response = http_request(endpoint: endpoint, method: GET)
return nil if @dry_run
data = JSON.parse(response.body)
return nil if data.empty?
if (data.length > 1)
raise NameError, "Did not find exactly one result for user #{username}. Found #{data.length} results"
end
id = data[0]['id']
USER_ID_CACHE[username] = id
id
end
# Get the ID for a given group on files.com
#
# @param group_name [String] the name of the group
#
# @return [Integer] the ID for the group or `nil` if the group does not exist
# @throws IOError if there is an error making the request or a non-2xx status is returned
def get_group_id(group_name)
LOGGER.info "Getting ID for group #{group_name}"
id = GROUP_ID_CACHE[group_name]
return id unless id.nil?
endpoint = "groups"
response = http_request(endpoint: endpoint, method: GET)
return nil if @dry_run
data = JSON.parse(response.body).select do |group|
group['name'] == group_name
end
return nil if data.empty?
if (data.length > 1)
raise NameError, "Did not find exactly one result for group #{group_name}. Found #{data.length} results"
end
id = data[0]['id']
GROUP_ID_CACHE[group_name] = id
id
end
end
class FilesDotCom::Runner < FilesDotCom
AUTOMATED_UPLOAD_FOLDER_NAME = "standard-uploads-for-ict"
AD_HOC_UPLOAD_FOLDER_NAME = "other-uploads-for-ict"
REPORT_FOLDER_NAME = "files-from-ict"
INTERNAL_UPLOADER_GROUP = "ict-internal_all-districts-files-from-ict-uploader"
attr_accessor :options,
:manager,
:district_root_folder,
:uploader_group_name,
:viewer_group_name,
:upload_notifications_group_name,
:report_notifications_group_name,
:automated_upload_path,
:ad_hoc_upload_path,
:report_upload_path
def initialize(options)
@options = options
@uploader_group_name = "#{@options.district}-data-uploader"
@viewer_group_name = "#{@options.district}_report-viewer"
@upload_notifications_group_name = "#{@options.district}_uploads-for-ict-notifications"
@report_notifications_group_name = "#{@options.district}_files-from-ict-notifications"
@district_root_folder = @options.district
@automated_upload_path = "#{@district_root_folder}/#{AUTOMATED_UPLOAD_FOLDER_NAME}"
@ad_hoc_upload_path = "#{@district_root_folder}/#{AD_HOC_UPLOAD_FOLDER_NAME}"
@report_upload_path = "#{@district_root_folder}/#{REPORT_FOLDER_NAME}"
@manager = FilesDotCom::Manager.new(api_key: @options.api_key, verbose: @options.verbose, dry_run: @options.dry_run)
end
def create_district_directories
@manager.create_directory(directory_name: @district_root_folder)
[@automated_upload_path, @ad_hoc_upload_path, @report_upload_path].each do |path|
@manager.create_directory(directory_name: path)
end
end
def create_district_permission_groups
[@viewer_group_name, @uploader_group_name].each do |group|
@manager.create_group(group_name: group)
end
end
def create_district_nofification_groups
[@report_notifications_group_name, @upload_notifications_group_name].each do |group|
@manager.create_group(group_name: group)
end
end
def add_district_group_permissions
@manager.add_permission_to_group(
group_name: @uploader_group_name,
directory_name: @automated_upload_path,
permission_type: "full"
)
@manager.add_permission_to_group(
group_name: @uploader_group_name,
directory_name: @ad_hoc_upload_path,
permission_type: "full"
)
manager.add_permission_to_group(
group_name: @viewer_group_name,
directory_name: @report_upload_path,
permission_type: "readonly"
)
end
def add_internal_group_permissions
manager.add_permission_to_group(
group_name: INTERNAL_UPLOADER_GROUP,
directory_name: @report_upload_path,
permission_type: "full"
)
end
def add_pm_to_district_notification_groups
@manager.add_user_to_group(username: @options.pm_username, group_name: @upload_notifications_group_name)
@manager.add_user_to_group(username: @options.pm_username, group_name: @report_notifications_group_name)
end
def add_pds_to_district_notification_groups
@options.pd_usernames.each do |pd_username|
@manager.add_user_to_group(username: pd_username, group_name: @upload_notifications_group_name)
end
end
def add_analysts_to_district_notification_groups
@options.analyst_usernames.each do |analyst_username|
@manager.add_user_to_group(username: analyst_username, group_name: @upload_notifications_group_name)
end
end
def add_district_group_notifications
@manager.add_notification_to_directory(
directory_name: @automated_upload_path,
group_name: @upload_notifications_group_name
)
@manager.add_notification_to_directory(
directory_name: @ad_hoc_upload_path,
group_name: @upload_notifications_group_name
)
@manager.add_notification_to_directory(
directory_name: @report_upload_path,
group_name: @report_notifications_group_name
)
end
def create_district_user
@manager.create_user(
username: @options.username,
full_name: @options.full_name,
email: @options.email,
user_type: @options.user_type
)
end
def add_district_user_public_key
@manager.add_public_key(username: @options.username, public_key_path: @options.public_key_path)
end
def add_district_user_to_groups
case @options.user_type
when :bot
when :uploader
manager.add_user_to_group(username: @options.username, group_name: uploader_group_name)
when :viewer
manager.add_user_to_group(username: @options.username, group_name: viewer_group_name)
when :uploader_viewer
manager.add_user_to_group(username: @options.username, group_name: uploader_group_name)
manager.add_user_to_group(username: @options.username, group_name: viewer_group_name)
else
raise ArgumentError, "Invalid user type: #{@options.user_type}"
end
end
def run
create_district_directories
create_district_permission_groups
create_district_nofification_groups
add_district_group_permissions
add_internal_group_permissions
add_district_group_notifications
add_pm_to_district_notification_groups unless @options.pm_username.nil?
add_pds_to_district_notification_groups
add_analysts_to_district_notification_groups
create_district_user
add_district_user_public_key unless @options.public_key_path.nil?
add_district_user_to_groups
end
end
class FilesDotCom::CLI < FilesDotCom
REQUIRED_ARGS = %i(district full_name email user_type)
VALID_USER_TYPES = %i(uploader viewer uploader_viewer bot)
API_KEY_KEY = "FILESDOTCOM_API_KEY"
DEFAULT_VERBOSE = false
DEFAULT_DRY_RUN = true
DEFAULT_ANALYST_USERNAMES = %w(in-class-today_brandon in-class-today_jessica)
DEFAULT_PD_USERNAMES = %w(in-class-today_santi in-class-today_manny)
OPTION_DELIMITER = ','
attr_accessor :options
def initialize
@options = parse_command_line_options
end
def run
FilesDotCom::Runner.new(@options).run
end
def parse_command_line_options
options = FilesDotCom::Options.new
options.verbose = DEFAULT_VERBOSE
options.dry_run = DEFAULT_DRY_RUN
options.analyst_usernames = DEFAULT_ANALYST_USERNAMES
options.pd_usernames = DEFAULT_PD_USERNAMES
OptionParser.new do |opts|
opts.banner = <<~HEREDOC
Automated account setup script for district users on files.com
Example usage:
./files-dot-com-account-setup.rb --district townerton-usa \\
--email john.fakenamington@example.com \\
--full-name "John Fakenamington" \\
--user-type uploader \\
--verbose \\
--pm-username in-class-today_some.user \\
--pd-usernames 'in-class-today_someone,in-class-today_someone.else'
--analyst-usernames '--pd-usernames 'in-class-today_analyst,in-class-today_other.analyst'
--public-key-path test_key.pub \\
--dry-run
HEREDOC
opts.on("-d DISTRICT", "--district DISTRICT", "District (required)") do |d|
options.district = d
end
opts.on("-f FULL_NAME", "--full-name FULL_NAME", "Full Name (required)") do |f|
options.full_name = f
end
opts.on("-email EMAIL", "--email EMAIL", "Email (required)") do |e|
options.email = e
end
opts.on("-t USER_TYPE", "--user-type USER_TYPE", "User Type (required); must be one of uploader, viewer, uploader_viewer, bot") do |t|
options.user_type = t.to_sym
end
opts.on("-k PUBLIC_KEY_PATH", "--public-key-path PUBLIC_KEY_PATH", "Public Key Path (optional)") do |k|
options.public_key_path = k
end
opts.on("-p PM_USERNAME", "--pm-username PM_USERNAME", "Program Manager Username (for notifiations)") do |p|
options.pm_username = p
end
opts.on("-P PD_USERNAMES", "--pd-usernames PD_USERNAME",
"Comma-delimited list of program delivery usernames who should receive notifications (optional)") do |p|
options.pd_usernames = (p.split OPTION_DELIMITER).map(&:strip)
end
opts.on("-a ANALYST_USERNAMES", "--analyst-usernames ANALYST_USERNAMES",
"Comma-delimited list of analyst usernames who should receive notifications (optional); Defaults to Manny and Santi") do |a|
options.analyst_usernames = (a.split OPTION_DELIMITER).map(&:strip)
end
opts.on("-v", "--[no-]verbose", "Run verbosely. Defaults to false") do |v|
options.verbose = v
end
opts.on("-D", "--[no-]dry-run", "Dry run (log commands but do not actually run them). Defaults to true") do |d|
options.dry_run = d
end
opts.on("-h", "--help", "Prints this help") do
puts opts
exit
end
end.parse!
REQUIRED_ARGS.each do |arg|
raise ArgumentError, "Argument #{arg} is required but was not provided" if(options[arg].nil?)
end
unless VALID_USER_TYPES.include? options.user_type
raise ArgumentError, "User type #{options.user_type} is invalid. Must be one of #{VALID_USER_TYPES.to_s}"
end
options.username = "#{options.district}_#{options.email.gsub(/@.*$/, "").downcase}"
if !options.public_key_path.nil? && !File.exist?(options.public_key_path)
raise ArgumentError, "Public key file #{options.public_key_path} does not exist"
end
if ENV[API_KEY_KEY].nil?
p "Input your files.com API key"
options.api_key = STDIN.noecho(&:gets).chomp
else
options.api_key = ENV[API_KEY_KEY]
end
options
end
end
FilesDotCom::CLI.new.run
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment