Using rubyzip to add files and nested directories to a zipoutputstream - ruby

I'm struggling with getting rubyzip to append directories to a zipoutputstream. (I want the output stream so I can send it from a rails controller). My code follows this example:
http://info.michael-simons.eu/2008/01/21/using-rubyzip-to-create-zip-files-on-the-fly/
When modified to include directories in the list of files to add I get the following error:
Any help would be greatly appreciated.
UPDATE
After trying a number of solutions I had best success with zipruby which has a clean api and good examples: http://zipruby.rubyforge.org/.

Zip::ZipFile.open(path, Zip::ZipFile::CREATE) do |zip|
songs.each do |song|
zip.add "record/#{song.title.parameterize}.mp3", song.file.to_file.path
end
end

OOOOOuuuhh...you DEFINITELY want ZIPPY. It's a Rails plugin that abstracts a lot of the complexity in rubyzip, and lets you create what you're talking about, including directories (from what I recall).
Here you go:
http://github.com/toretore/zippy
And direct from the zippy site:
Example controller:
def show
#gallery = Gallery.find(params[:id])
respond_to do |format|
format.html
format.zip
end
end
Example view:
zip['description.txt'] = #gallery.description
#gallery.photos.each do |photo|
zip["photo_#{photo.id}.png"] = File.open(photo.url)
end
edit: Amending per user comment:
Hmm...the whole objective of using Zippy is to make it a whole lot easier to use ruby zip.
Ya might want to take a second (or first) look...
Here's how to make a directory with directories:
some_var = Zippy.open('awsum.zip') do |zip|
%w{dir_a dir_b dir_c diri}.each do |dir|
zip["bin/#{dir}/"]
end
end
...
send_file some_var, :file_name => ...

Zippy will work for this. There may be a more cool way to do this but since there are essentially no docs, here's what I came up with for recursively copying directories with Zippy in a Rakefile. This Rakefile is used in a Rails environment so I put gem requirements in my Gemfile:
#Gemfile
source 'http://rubygems.org'
gem 'rails'
gem 'zippy'
And this is the Rakefile
#Rakefile
def add_file( zippyfile, dst_dir, f )
zippyfile["#{dst_dir}/#{f}"] = File.open(f)
end
def add_dir( zippyfile, dst_dir, d )
glob = "#{d}/**/*"
FileList.new( glob ).each { |f|
if (File.file?(f))
add_file zippyfile, dst_dir, f
end
}
end
task :myzip do
Zippy.create 'my.zip' do |z|
add_dir z, 'my', 'app'
add_dir z, 'my', 'config'
#...
add_file z, 'my', 'config.ru'
add_file z, 'my', 'Gemfile'
#...
end
end
Now I can use it like this:
C:\> cd my
C:\my> rake myzip
and it will produce my.zip which contains an inner directory called 'my' with copies of selected files and directories.

I was able to get directories working with the same ZipOutputStream used in the original article.
All I had to do was add the directory when calling zos.put_next_entry.
For example:
require 'zip/zip'
require 'zip/zipfilesystem'
t = Tempfile.new("some-weird-temp-file-basename-#{request.remote_ip}")
# Give the path of the temp file to the zip outputstream, it won't try to open it as an archive.
Zip::ZipOutputStream.open(t.path) do |zos|
some_file_list.each do |file|
# Create a new entry with some arbitrary name
zos.put_next_entry("myfolder/some-funny-name.jpg") # Added myfolder/
# Add the contents of the file, don't read the stuff linewise if its binary, instead use direct IO
zos.print IO.read(file.path)
end
end
# End of the block automatically closes the file.
# Send it using the right mime type, with a download window and some nice file name.
send_file t.path, :type => 'application/zip', :disposition => 'attachment', :filename => "some-brilliant-file-name.zip"
# The temp file will be deleted some time...
t.close
I just changed zos.put_next_entry('some-funny-name.jpg') to zos.put_next_entry('myfolder/some-funny-name.jpg'), and the resulting zipfile had a nested folder called myfolder that contained the files.

Related

Ruby: check if a .zip file exists, and extract

2 small questions to create the effect I'm looking for.
How do I check if a file exists within a directory with the extension of .zip?
If it does exist I need to make a folder with the same name as the .zip without the .zip extension for the folder.
Then I need to extract the files into the folder.
Secondly, what do I do if there are more than one .zip files in the folder?
I'm doing something like this and trying to put it into ruby
`mkdir fileNameisRandom`
`unzip fileNameisRandom.zip -d fileNameisRandom`
On a similar post I found something like
Dir.entries("#{Dir.pwd}").select {|f| File.file? f}
which I know checks all files within a directory and makes sure they are a file.
The problem is I don't know how to make sure that it is only an extension of .zip
Also, I found the Glob function which checks the extension of a filename from: http://ruby-doc.org/core-1.9.3/Dir.html
How do I ensure the file exists in that case, and if it doesn't I can print out an error then.
From the comment I now have
if Dir['*.zip'].first == nil #check to see if any exist
puts "A .zip file was not found"
elsif Dir['*.zip'].select {|f| File.file? f} then #ensure each of them are a file
#use a foreach loop to go through each one
Dir['*.zip'].select.each do |file|
puts "#{file}"
end ## end for each loop
end
Here's a way of doing this with less branching:
# prepare the data
zips= Dir['*.zip'].select{ |f| File.file? }
# check if data is sane
if zips.empty?
puts "No zips"
exit 0 # or return
end
# process data
zips.each do |z|
end
This pattern is easier to follow for fellow programmers.
You can also do it using a ruby gem called rubyzip
Gemfile:
source 'https://rubygems.org'
gem 'rubyzip'
run bundle
unzip.rb:
require 'zip'
zips= Dir['*.zip'].select{ |f| File.file? }
if zips.empty?
puts "No zips"
exit 0 # or return
end
zips.each do |zip|
Zip::File.open(zip) do |files|
files.each do |file|
# write file somewhere
# see here https://github.com/rubyzip/rubyzip
end
end
end
I finally pieced together different information from tutorials and used #rogerdpack and his comment for help.
require 'rubygems/package'
#require 'zlib'
require 'fileutils'
#move to the unprocessed directory to unpack the files
#if a .tgz file exists
#take all .tgz files
#make a folder with the same name
#put all contained folders from .tgz file inside of similarly named folder
#Dir.chdir("awaitingApproval/")
if Dir['*.zip'].first == nil #check to see if any exist, I use .first because Dir[] returns an array
puts "A .zip file was not found"
elsif Dir['*.zip'].select {|f| File.file? f} then #ensure each of them are a file
#use a foreach loop to go through each one
Dir['*.zip'].select.each do |file|
puts "" #newlie for each file
puts "#{file}" #print out file name
#next line based on `mkdir fileNameisRandom`
`mkdir #{Dir.pwd}/awaitingValidation/#{ File.basename(file, File.extname(file)) }`
#next line based on `unzip fileNameisRandom.zip -d fileNameisRandom`
placement = "awaitingValidation/" + File.basename(file, File.extname(file))
puts "#{placement}"
`sudo unzip #{file} -d #{placement}`
puts "Unzip complete"
end ## end for each loop
end

Get file name and extension in Ruby

I'm working on a program to download a video from YouTube, convert it to MP3 and create a directory structure for the files.
My code is:
FileUtils.cd("#{$musicdir}/#{$folder}") do
YoutubeDlhelperLibs::Downloader.get($url)
if File.exists?('*.mp4')
puts 'Remove unneeded tempfile'
Dir['*.mp4'].each do |waste|
File.delete(waste)
end
else
puts 'Temporary file already deleted'
end
Dir['*.m4a'].each do |rip|
rip.to_s
rip.split
puts 'Inside the function'
puts rip
end
end
The first one goes to the already created music folder. Inside that I'm executing get. After that I have two files in the directory: "xyz.mp4" and "xyz.m4a".
I would like to fetch the filename without the extension so I can handle both files differently.
I'm using an array, but an array for just one match sounds crazy for me.
Has anyone another idea?
You can use the following functions for your purpose:
path = "/path/to/xyz.mp4"
File.basename(path) # => "xyz.mp4"
File.extname(path) # => ".mp4"
File.basename(path, ".mp4") # => "xyz"
File.basename(path, ".*") # => "xyz"
File.dirname(path) # => "/path/to"

unzipping a zip archive from a string

I have a zip archive in a string, but the rubyzip gem appears to want input from a file. The best I've come up with is to write the zip archive to a tempfile for the sole purpose of passing the filename to Zip::ZipFile.foreach(), but this seems tortured:
require 'zip/zip'
def unzip(page)
"".tap do |str|
Tempfile.open("unzip") do |tmpfile|
tmpfile.write(page)
Zip::ZipFile.foreach(tmpfile.path()) do |zip_entry|
zip_entry.get_input_stream {|io| str << io.read}
end
end
end
end
Is there a simpler way?
NOTE: See also Ruby Unzip String.
See Zip/Ruby Zip::Archive.open_buffer(...):
require 'zipruby'
Zip::Archive.open_buffer(str) do |archive|
archive.each do |entry|
entry.name
entry.read
end
end
#maerics's answer introduced me to the zipruby gem (not to be confused with the rubyzip gem). It works well. My complete code ended up like this:
require 'zipruby'
# Given a string in zip format, return a hash where
# each key is an zip archive entry name and each
# value is the un-zipped contents of the entry
def unzip(zipfile)
{}.tap do |entries|
Zip::Archive.open_buffer(zipfile) do |archive|
archive.each do |entry|
entries[entry.name] = entry.read
end
end
end
end
Ruby's StringIO would help in this case.
Think of it as a string/buffer you can treat like an in-memory file.

How do I create directory if none exists using File class in Ruby?

I have this statement:
File.open(some_path, 'w+') { |f| f.write(builder.to_html) }
Where
some_path = "somedir/some_subdir/some-file.html"
What I want to happen is, if there is no directory called somedir or some_subdir or both in the path, I want it to automagically create it.
How can I do that?
You can use FileUtils to recursively create parent directories, if they are not already present:
require 'fileutils'
dirname = File.dirname(some_path)
unless File.directory?(dirname)
FileUtils.mkdir_p(dirname)
end
Edit: Here is a solution using the core libraries only (reimplementing the wheel, not recommended)
dirname = File.dirname(some_path)
tokens = dirname.split(/[\/\\]/) # don't forget the backslash for Windows! And to escape both "\" and "/"
1.upto(tokens.size) do |n|
dir = tokens[0...n]
Dir.mkdir(dir) unless Dir.exist?(dir)
end
For those looking for a way to create a directory if it doesn't exist, here's the simple solution:
require 'fileutils'
FileUtils.mkdir_p 'dir_name'
Based on Eureka's comment.
directory_name = "name"
Dir.mkdir(directory_name) unless File.exists?(directory_name)
How about using Pathname?
require 'pathname'
some_path = Pathname("somedir/some_subdir/some-file.html")
some_path.dirname.mkdir_p
some_path.write(builder.to_html)
Based on others answers, nothing happened (didn't work). There was no error, and no directory created.
Here's what I needed to do:
require 'fileutils'
response = FileUtils.mkdir_p('dir_name')
I needed to create a variable to catch the response that FileUtils.mkdir_p('dir_name') sends back... then everything worked like a charm!
Along similar lines (and depending on your structure), this is how we solved where to store screenshots:
In our env setup (env.rb)
screenshotfolder = "./screenshots/#{Time.new.strftime("%Y%m%d%H%M%S")}"
unless File.directory?(screenshotfolder)
FileUtils.mkdir_p(screenshotfolder)
end
Before do
#screenshotfolder = screenshotfolder
...
end
And in our hooks.rb
screenshotName = "#{#screenshotfolder}/failed-#{scenario_object.title.gsub(/\s+/,"_")}-#{Time.new.strftime("%Y%m%d%H%M%S")}_screenshot.png";
#browser.take_screenshot(screenshotName) if scenario.failed?
embed(screenshotName, "image/png", "SCREENSHOT") if scenario.failed?
The top answer's "core library" only solution was incomplete. If you want to only use core libraries, use the following:
target_dir = ""
Dir.glob("/#{File.join("**", "path/to/parent_of_some_dir")}") do |folder|
target_dir = "#{File.expand_path(folder)}/somedir/some_subdir/"
end
# Splits name into pieces
tokens = target_dir.split(/\//)
# Start at '/'
new_dir = '/'
# Iterate over array of directory names
1.upto(tokens.size - 1) do |n|
# Builds directory path one folder at a time from top to bottom
unless n == (tokens.size - 1)
new_dir << "#{tokens[n].to_s}/" # All folders except innermost folder
else
new_dir << "#{tokens[n].to_s}" # Innermost folder
end
# Creates directory as long as it doesn't already exist
Dir.mkdir(new_dir) unless Dir.exist?(new_dir)
end
I needed this solution because FileUtils' dependency gem rmagick prevented my Rails app from deploying on Amazon Web Services since rmagick depends on the package libmagickwand-dev (Ubuntu) / imagemagick (OSX) to work properly.

how to store the name of nested files in a variable and loop through them in rake

I have the following rake file to create a static version of my sinatra app,
stolen from http://github.com/semanticart/stuff-site/blob/master/Rakefile
class View
attr_reader :permalink
def initialize(path)
filename = File.basename(path)
#permalink = filename[0..-6]
end
end
view_paths = Dir.glob(File.join(File.dirname(__FILE__), 'views/pages', '*.haml'))
ALL_VIEWS = view_paths.map {|path| View.new(path) }
task :build do
def dump_request_to_file url, file
Dir.mkdir(File.dirname(file)) unless File.directory?(File.dirname(file))
File.open(file, 'w'){|f| f.print #request.get(url).body}
end
static_dir = File.join(File.dirname(__FILE__), 'public')
require 'sinatra'
require 'c4eo'
#request = Rack::MockRequest.new(Sinatra::Application)
ALL_VIEWS.each do |view|
puts view
dump_request_to_file("/#{view.permalink}", File.join(static_dir, view.permalink+'.html'))
end
end
ALL_VIEWS is now an array containing all the Haml files in the root of my 'views/pages' directory.
How do I modify ALL_VIEWS and the dump_request_to_file method to cycle through all the subdirectories in my views/pages directory?
My views directory looks a bit like this: http://i45.tinypic.com/167unpw.gif
If it makes life a lot easier, I could have all my files named index.haml, inside directories.
Thanks
To cycle through all subdirs, change 'views/pages' to 'views/pages/**'
The double splats tells it to search recursively, you can see it in the docs at
http://ruby-doc.org/core/classes/Dir.html#M002322
Note that I haven't looked thoroughly at your use case, but preliminarily it appears that you may have trouble generating a permalink. When I checked the results, I got:
[#<View:0x1010440a0 #permalink="hound">,
#<View:0x101044078 #permalink="index">,
#<View:0x101044000 #permalink="hound">,
#<View:0x101043f88 #permalink="index">,
#<View:0x101043f10 #permalink="references">,
#<View:0x101043e98 #permalink="do_find">,
#<View:0x101043e20 #permalink="index">,
#<View:0x101043da8 #permalink="README">]
Which were generated from these files:
["/Users/josh/deleteme/fileutilstest/views/pages/bar/cheese/rodeo/hound.haml",
"/Users/josh/deleteme/fileutilstest/views/pages/bar/cheese/rodeo/outrageous/index.haml",
"/Users/josh/deleteme/fileutilstest/views/pages/bar/pizza/hound.haml",
"/Users/josh/deleteme/fileutilstest/views/pages/bar/pizza/index.haml",
"/Users/josh/deleteme/fileutilstest/views/pages/bar/pizza/references.haml",
"/Users/josh/deleteme/fileutilstest/views/pages/do_find.haml",
"/Users/josh/deleteme/fileutilstest/views/pages/tutorials/index.haml",
"/Users/josh/deleteme/fileutilstest/views/pages/tutorials/README.haml"]
And it looks like you create the link with:
File.join(static_dir, view.permalink+'.html')
So you can see that in this case, that would create three files like static_dir/index.html
A fairly obvious solution is to include the relative portion of the link, so it would become
static_dir/bar/cheese/rodeo/outrageous/index.html
static_dir/bar/pizza/index.html
static_dir/tutorials/index.html
EDIT: In regards to addressing how to find the relative url, this seems to work
class View
attr_reader :permalink
def initialize( root_path , path )
root_path = File.expand_path(root_path).sub(/\/?$/,'/')
path = File.expand_path path
filename = path.gsub root_path , String.new
raise "#{path} does not appear to be a subdir of #{root_path}" unless root_path + filename == path
#permalink = filename[0..-6]
end
end
view_paths = Dir.glob(File.join(File.dirname(__FILE__), 'views/pages/**', '*.haml'))
ALL_VIEWS = view_paths.map { |path| View.new 'views/pages' , path }
require 'pp'
pp ALL_VIEWS
I'm not all that keen on the [0..-6] thing, it only works if you know your file has a suffix and that it is five characters long. But I'm going to leave it alone since I don't really know how you would want to handle the different future situations I might anticipate (ie generate an html from the haml and serve that up, now you have two files index.html and index.haml, which, after you remove their extensions, are both just index. Or styles.css which loses part of its filename when you attempt to remove its extension by pulling in [0..-6]

Resources