How can I detect whether a file is writeable in Ruby? - ruby

The function should return false if any of these are true:
The complete path is an existing directory.
The path is not a legal file name (invalid characters, too long, etc.)
The path refers to an existing file that is not writeable by current user.
The path includes any directory segments that do not already exist.
The function should return true if all of these are true:
All path segments except the file name are already existing directories.
The file either does not already exist or exists and is writable by the current user.
The directory that will hold the file is writable by the current user.
The path segments and filename are not too long and are composed only of valid characters for filenames.
A relative or absolute path is specified.
The function must work on MAC OSX using Ruby 1.9.3.
The following method returns false when the file does not already exist and should be writable, even though I am running it from a subdirectory of my own user directory:
File.writable?('./my-file-name')
I will accept a solution involving calling file open and catching the exception if a write fails, though I prefer to not rely on exceptions.
Here is what I have so far, but it does not handle some edge cases:
def file_writable?(filename)
return false if (filename || '').size == 0
return false if File.directory?(filename)
return false if File.exists?(filename) && !File.writable(filename)
return true;
end
I still do not know how to conveniently test if any directory segments are missing, or if weird characters are in the name, and some other cases.

Ruby includes Pathname in its std-lib, which is a wrapper around several other file-oriented classes:
All functionality from File, FileTest, and some from Dir and FileUtils is included, in an unsurprising way. It is essentially a facade for all of these, and more.
A "pathname" can be a directory or a file.
Pathname doesn't do all the things you require, however it acts as a very convenient starting point. You can easily add the additional functionality you need since it is a wrapper, much easier than if you tried to implement a solution using the individual classes.
It's my go-to class when I have to do a lot of directory/file processing.
require 'pathname' # => true
foo = Pathname.new('/usr/bin/ruby') # => #<Pathname:/usr/bin/ruby>
foo.writable? # => false
foo.directory? # => false
foo.exist? # => true
tmp = Pathname.new('/tmp/foo') # => #<Pathname:/tmp/foo>
tmp.write('bar') # => 3
tmp.writable? # => true
tmp.directory? # => false
tmp.exist? # => true
bin = Pathname.new('/usr/bin') # => #<Pathname:/usr/bin>
bin.writable? # => false
bin.directory? # => true
bin.exist? # => true
fake = Pathname.new('/foo/bar') # => #<Pathname:/foo/bar>
fake.exist? # => false
You can't tell what components are missing from a directory, but normally we'd try to create it and rescue the exception if it occurs, dealing with permission errors. It wouldn't be hard to write code to look for a full directory path, then iterate though the chain of directories if the full-path doesn't exist, looking for each as a child of the previous one. Enumerable's find and find_all would be useful.

Related

How do you update an environment variable using Ruby and Chef?

I know how to set the variables for both user and machine.
The problem arises when I try to add to the PATH. Currently my code will overwrite what is in the PATH.
execute 'set java_home2' do
command "setx -m PATH2 \"D:\\Home"
*only_if {"PATH2" == " "}*
end
This currently ensures that the PATH will only run if there is no PATH. When the only_if is removed the problem of overwriting arises.
EDIT:
I am now able to modify the system variable but cannot work out how to do the same with the user variables
env 'path addition' do
key_name "PATH"
value (ENV["PATH"] + ";D:\\Home\\Apps\\variable")
:modify
end
From the question, it looks like you are trying to add PATH on windows server. In that case you can use windows cookbook resource called windows_path for such operation:
windows_path 'C:\Sysinternals' do
action :add
end
https://github.com/chef-cookbooks/windows
https://supermarket.chef.io/cookbooks/windows
I can't speak for specifics in chef, but in ruby, you can access environment variables with the ENV hash. So for PATH, you could do the following:
ENV["PATH"] = ENV["PATH"].split(":").push("/my/new/path").join(":")
That will update your PATH for the duration of the program's execution. Keep in mind that:
This will only update PATH for your ruby script, and only temporarily. Permanently changing your PATH is more complicated and dependent on OS.
This code assumes you're using linux. In windows, the PATH delimiter is ; instead of :, so you should update the code accordingly.
I found the answer:
#Append notepad to user PATH variable
registry_key "HKEY_CURRENT_USER\\Environment" do
$path_name = ""
subkey_array = registry_get_values("HKEY_CURRENT_USER\\Environment", :x86_64)
subkey_array.each{ |val|
case val[:name].include?("PATH")
when true
$path_name = val[:data]
print "\n The User PATH is: #{$path_name}"
break
when false
print ':'
end
}
values [{
:name => "PATH",
:type => :string,
:data => "#{$path_name};D:\\Home\\Apps\\Notepad++\\Notepad++"
}]
action :create
#add a guard to prevent duplicates
not_if {
$path_name.include?("D:\\Home\\Apps\\Notepad++\\Notepad++")
}
end
This code when ran from the CMD line will print the current User PATH variables, then it will append D:/Home/Apps/Notepad++/Notepad++ IF it is not currently in the PATH. If it already exists then this will be skipped.

Normalize HTTP URI

I get URIs from Akamai's log files that include entries such as the following:
/foo/jim/jam
/foo/jim/jam?
/foo/./jim/jam
/foo/bar/../jim/jam
/foo/jim/jam?autho=<randomstring>&file=jam
I would like to normalize all of these to the same entry, under the rules:
If there is a query string, strip autho and file from it.
If the query string is empty, remove the trailing ?.
Directory entries for ./ should be removed.
Directory entries for <fulldir>/../ should be removed.
I would have thought that the URI library for Ruby would cover this, but:
It does not provide any mechanism for parsing parts of the query string. (Not that this is hard to do, nor standard.)
It does not remove a trailing ? if the query string is emptied.
URI.parse('/foo?jim').tap{ |u| u.query='' }.to_s #=> "/foo?"
The normalize method does not clean up . or .. in the path.
So, failing an official library, I find myself writing a regex-based solution.
def normalize(path)
result = path.dup
path.sub! /(?<=\?).+$/ do |query|
query.split('&').reject do |kv|
%w[ autho file ].include?(kv[/^[^=]+/])
end.join('&')
end
path.sub! /\?$/, ''
path.sub!(/^[^?]+/){ |path| path.gsub(%r{[^/]+/\.\.},'').gsub('/./','/') }
end
It happens to work for the test cases I've listed above, but with 450,000 paths to clean up I cannot hand check them all.
Is there any glaring error with the above, considering likely log file entries?
Is there a better way to accomplish the same that leans on proven parsing techniques instead of my hand-rolled regex?
The addressable gem will normalize these for you:
require 'addressable/uri'
# normalize relative paths
uri = Addressable::URI.parse('http://example.com/foo/bar/../jim/jam')
puts uri.normalize.to_s #=> "http://example.com/foo/jim/jam"
# removes trailing ?
uri = Addressable::URI.parse('http://example.com/foo/jim/jam?')
puts uri.normalize.to_s #=> "http://example.com/foo/jim/jam"
# leaves empty parameters alone
uri = Addressable::URI.parse('http://example.com/foo/jim/jam?jim')
puts uri.normalize.to_s #=> "http://example.com/foo/jim/jam?jim"
# remove specific query parameters
uri = Addressable::URI.parse('http://example.com/foo/jim/jam?autho=<randomstring>&file=jam')
cleaned_query = uri.query_values
cleaned_query.delete('autho')
cleaned_query.delete('file')
uri.query_values = cleaned_query
uri.normalize.to_s #=> "http://example.com/foo/jim/jam"
Something that is REALLY important, like, ESSENTIAL to remember, is that a URL/URI is a protocol, a host, a file-path to a resource, followed by options/parameters being passed to the resource being referenced. (For the pedantic, there are other, optional, things in there too but this is sufficient.)
We can extract the path from a URL by parsing it using the URI class, and using the path method. Once we have the path, we have either an absolute path or a relative path based on the root of the site. Dealing with absolute paths is easy:
require 'uri'
%w[
/foo/jim/jam
/foo/jim/jam?
/foo/./jim/jam
/foo/bar/../jim/jam
/foo/jim/jam?autho=<randomstring>&file=jam
].each do |url|
uri = URI.parse(url)
path = uri.path
puts File.absolute_path(path)
end
# >> /foo/jim/jam
# >> /foo/jim/jam
# >> /foo/jim/jam
# >> /foo/jim/jam
# >> /foo/jim/jam
Because the paths are file paths based on the root of the server, we can play games using Ruby's File.absolute_path method to normalize the '.' and '..' away and get a true absolute path. This will break if there are more .. (parent directory) than the chain of directories, but you shouldn't find that in extracted paths since that would also break the server/browser ability to serve/request/receive resources.
It gets a bit more "interesting" when dealing with relative paths but File is still our friend then, but that's a different question.

Retrieve a file in Ruby

So what I am trying to do is pass a file name into a method and and check if the file is closed. What I am struggling to do is getting a file object from the file name without actually opening the file.
def file_is_closed(file_name)
file = # The method I am looking for
file.closed?
end
I have to fill in the commented part. I tried using the load_file method from the YAML module but I think that gives the content of the file instead of the actual file.
I couldn't find a method in the File module to call. Is there a method maybe that I don't know?
File#closed? returns whether that particular File object is closed, so there is no method that is going to make your current attempted solution work:
f1 = File.new("test.file")
f2 = File.new("test.file")
f1.close
f1.closed? # => true # Even though f2 still has the same file open
It would be best to retain the File object that you're using in order to ask it if it is closed, if possible.
If you really want to know if your current Ruby process has any File objects open for a particular path, something like this feels hack-ish but should mostly work:
def file_is_closed?(file_name)
ObjectSpace.each_object(File) do |f|
if File.absolute_path(f) == File.absolute_path(file_name) && !f.closed?
return false
end
end
true
end
I don't stand by that handling corner cases well, but it seems to work for me in general:
f1 = File.new("test.file")
f2 = File.new("test.file")
file_is_closed?("test.file") # => false
f1.close
file_is_closed?("test.file") # => false
f2.close
file_is_closed?("test.file") # => true
If you want to know if any process has the file open, I think you'll need to resort to something external like lsof.
For those cases where you no longer have access to the original file objects in Ruby (after fork + exec, for instance), a list of open file descriptors is available in /proc/pid/fd. Each file there is named for the file descriptor number, and is a symlink to the opened file, pipe, or socket:
# Returns hash in form fd => filename
def open_file_descriptors
Hash[
Dir.glob( File.join( '/proc', Process.pid.to_s, 'fd', '*' ) ).
map { |fn| [File.basename(fn).to_i, File.readlink(fn)] rescue [nil, nil] }.
delete_if { |fd, fn| fd.nil? or fd < 3 }
]
end
# Return IO object for the named file, or nil if it's not open
def io_for_path(path)
fd, fn = open_file_descriptors.find {|k,v| path === v}
fd.nil? ? nil : IO.for_fd(fd)
end
# close an open file
file = io_for_path('/my/open/file')
file.close unless file.nil?
The open_file_descriptors method parses the fd directory and returns a hash like {3 => '/my/open/file'}. It is then a simple matter to get the file descriptor number for the desired file, and have Ruby produce an IO object for it with for_fd.
This assumes you are on Linux, of course.

How do I copy file contents to another file?

As basic as this seems, I simply can't manage to copy the contents of one file to another. Here is my code thus far:
#!/usr/bin/ruby
Dir.chdir( "/mnt/Shared/minecraft-server/plugins/Permissions" )
flist = Dir.glob( "*" )
flist.each do |mod|
mainperms = File::open( "AwesomeVille.yml" )
if mod == "AwesomeVille.yml"
puts "Shifting to next item..."
shift
else
File::open( mod, "w" ) do |newperms|
newperms << mainperms
end
end
puts "Updated #{ mod } with the contents of #{ mainperms }."
end
Why copy the contents of one file to another? Why not use either the OS to copy the file, or use Ruby's built-in FileUtils.copy_file?
ri FileUtils.copy_file
FileUtils.copy_file
(from ruby core)
------------------------------------------------------------------------------
copy_file(src, dest, preserve = false, dereference = true)
------------------------------------------------------------------------------
Copies file contents of src to dest. Both of src and
dest must be a path name.
A more flexible/powerful alternate is to use Ruby's built-in FileUtils.cp:
ri FileUtils.cp
FileUtils.cp
(from ruby core)
------------------------------------------------------------------------------
cp(src, dest, options = {})
------------------------------------------------------------------------------
Options: preserve noop verbose
Copies a file content src to dest. If dest is a
directory, copies src to dest/src.
If src is a list of files, then dest must be a directory.
FileUtils.cp 'eval.c', 'eval.c.org'
FileUtils.cp %w(cgi.rb complex.rb date.rb), '/usr/lib/ruby/1.6'
FileUtils.cp %w(cgi.rb complex.rb date.rb), '/usr/lib/ruby/1.6', :verbose => true
FileUtils.cp 'symlink', 'dest' # copy content, "dest" is not a symlink
This works for me
IO.copy_stream mainperms, mod
ยง copy_stream
I realize that this isn't the completely approved way, but
IO.readlines(filename).join('') # join with an empty string because readlines includes its own newlines
Will load a file into a string, which you can then output into newperms just like it was a string. There's good chance the reason this isn't working currently is that you are trying to write an IO handler to a file, and the IO handler isn't getting converted into a string in the way you want it to.
However, another fix might be
newperms << mainperms.read
Also, make sure you close mainperms before the script exits, as it might break something if you don't.
Hope this helps.

How to check if a directory/file/symlink exists with one command in Ruby

Is there a single way of detecting if a directory/file/symlink/etc. entity (more generalized) exists?
I need a single function because I need to check an array of paths that could be directories, files or symlinks. I know File.exists?"file_path" works for directories and files but not for symlinks (which is File.symlink?"symlink_path").
The standard File module has the usual file tests available:
RUBY_VERSION # => "1.9.2"
bashrc = ENV['HOME'] + '/.bashrc'
File.exist?(bashrc) # => true
File.file?(bashrc) # => true
File.directory?(bashrc) # => false
You should be able to find what you want there.
OP: "Thanks but I need all three true or false"
Obviously not. Ok, try something like:
def file_dir_or_symlink_exists?(path_to_file)
File.exist?(path_to_file) || File.symlink?(path_to_file)
end
file_dir_or_symlink_exists?(bashrc) # => true
file_dir_or_symlink_exists?('/Users') # => true
file_dir_or_symlink_exists?('/usr/bin/ruby') # => true
file_dir_or_symlink_exists?('some/bogus/path/to/a/black/hole') # => false
Why not define your own function File.exists?(path) or File.symlink?(path) and use that?
Just File.exist? on it's own will take care of all of the above for you

Resources