#!/usr/bin/ruby -w require "digest/md5" require "fileutils" require "set" $cc_license_html = < Creative Commons License
This work is licensed under a Creative Commons License. EOF class Renderer # FIXME: all the thumbnailers aren't save to use on filenames with # spaces and such def initialize() end def render(file, type) ### Read file and apply thumbnailer for type, return either a list ### of renders of the images, [] if the image is itself a render ### or nil on error (type not known and such) case type when "blend" return render_blend(file) when "xcf" return render_xcf(file) when "png", "jpg" return render_image(file) else return nil end end def render_self(file) # Used for images that don't need to render return [] end def render_image(file) output = IO.popen("convert -sample '512x512' '#{file}' 'jpg:/dev/stdout'").read() return [output] end def render_xcf(xcf) out = "/tmp/out.jpg" system("/usr/bin/xcftopnm '#{xcf}' | ppmtojpeg > '#{out}'") return [File.new(out).read()] end def render_blend(blend) thumb = "/tmp/out/0001" system("blender", blend, "-P", "blender_thumb.py") return [File.new(thumb).read()] end end class Thumbnailer def initialize() end def thumbnail(file) output = IO.popen("convert -sample '128x128' '#{file}' 'jpg:/dev/stdout'").read() return output end end class MultiRepository def initialize() @repos = [] end def add(repo) @repo.add(repo) end def each() if block_given? then @repos.each { |i| i.each { yield() }} end end def get(md5) @repos.each { |i| res = i.get if res != nil then return res end } return nil end def create_entry_from_data(data) end def create_entry_from_file(filename) end end class Repository attr_reader :path def initialize(path) @path = path end def has(md5) return (not(get(md5) == nil)) end def each() if block_given? then Dir.new(@path).each { |i| if (i != "." && i != ".." && i != "index.html") yield(i) end } end end def get(md5) entry = Entry.new(md5) if not File.exist?(entry.entrypath) then return nil else return entry end end def create_entry_from_data(data) md5 = Digest::MD5.hexdigest(data) entrypath = @path + "/" + md5 # Write begin FileUtils.mkdir(entrypath) f = File.new(entrypath + "/" + "=data", "w") f.write(data) f.close() # Write time f = File.new(entrypath + "/" + "=ctime", "w") f.write(Time.now.to_i.to_s) f.close() rescue Errno::EEXIST # Do nothing if entry is already there end return $repository.get(md5) end def create_entry_from_file(filename) entry = create_entry_from_data(File.new(filename).read()) # Write type type = File.extname(filename) if (not type.empty?) then f = File.new(entry.path + "/" + "=type", "w") f.write(type[1, type.length]) f.close() end entry.filename = File.basename(filename) return entry end end class Entry attr_reader :md5, :entrypath # Get a handle to an entry without error checking, use $repository.get instead def initialize(md5) @md5 = md5 @entrypath = $repository_path + "/" + @md5 end def path() return @entrypath end def print_name() return self.name || self.filename || self.md5 end def size() return File.size(@entrypath + "/" + "=data") end def special=(value) f = File.new(@entrypath + "/" + "=special", "w") f.write(value) f.close() end def special() begin return File.new(@entrypath + "/" + "=special", "r").read() rescue ArgumentError, Errno::ENOENT return nil end end def autogenerated=(value) f = File.new(@entrypath + "/" + "=autogenerated", "w") if value then f.write(1) else f.write(0) end f.close() end def autogenerated() begin if File.new(@entrypath + "/" + "=autogenerated", "r").read().to_i != 0 return true else return false end rescue ArgumentError, Errno::ENOENT return false end end def check() begin return (Digest::MD5.hexdigest(File.new(@entrypath + "/" + "=data").read()) == @md5) rescue return false end end def data() begin return File.new(@entrypath + "/" + "=data", "r").read() rescue ArgumentError, Errno::ENOENT return nil end end def parent=(value) f = File.new(@entrypath + "/" + "=parent", "w") f.write(value) f.close() end def parent() begin return File.new(@entrypath + "/" + "=parent", "r").read() rescue ArgumentError, Errno::ENOENT return nil end end def author=(value) f = File.new(@entrypath + "/" + "=author", "w") f.write(value) f.close() end def author() begin return File.new(@entrypath + "/" + "=author", "r").read() rescue ArgumentError, Errno::ENOENT return "" end end def name=(value) f = File.new(@entrypath + "/" + "=name", "w") f.write(value) f.close() end def name() begin return File.new(@entrypath + "/" + "=name", "r").read() rescue ArgumentError, Errno::ENOENT return nil end end def type() begin return File.new(@entrypath + "/" + "=type", "r").read() rescue ArgumentError, Errno::ENOENT return nil end end def type=(value) f = File.new(@entrypath + "/" + "=type", "w") f.write(value) f.close() end def filename=(value) f = File.new(@entrypath + "/" + "=filename", "w") f.write(value) f.close() end def filename() begin return File.new(@entrypath + "/" + "=filename", "r").read() rescue ArgumentError, Errno::ENOENT return nil end end def ctime() begin return Time.at(Integer(File.new(@entrypath + "/" + "=ctime", "r").read())) rescue ArgumentError, Errno::ENOENT return nil end end def read_prop_list(prop) begin return File.new(@entrypath + "/" + "=" + prop, "r").read().split() rescue ArgumentError, Errno::ENOENT return [] end end def write_prop_list(prop, value) f = File.new(@entrypath + "/" + "=" + prop, "w") value.each { |i| f.write(i) f.write(" ") } f.close() end def renders() read_prop_list("renders") end def renders=(value) write_prop_list("renders", value) end def render_add(text) a = self.renders r = Set.new(a) r.add(text) self.renders = r.to_a end def thumbnails() read_prop_list("thumbnails") end def thumbnails=(value) write_prop_list("thumbnails", value) end def thumbnail_add(text) a = self.thumbnails() r = Set.new(a) r.add(text) self.thumbnails = r.to_a end def related() read_prop_list("related").sort() end def related=(value) write_prop_list("related", value) end def related_add(text) if (text != md5) then a = self.related() r = Set.new(a) r.add(text) self.related = r.to_a end end def keywords() read_prop_list("keywords") end def keywords=(value) write_prop_list("keywords", value) end end def print_help() puts "Usage: mediarepo COMMAND [ARGS]" puts "" puts "Commands:" puts "=========" puts " add " puts " add the file given by to the repository" puts "" puts " get " puts " retrieve an entry from the database and output it to stdout" puts "" puts " propset " puts " set the property to for item " puts "" puts " show :" puts " display item with id" puts "" puts " clean:" puts " removes dangling references from the db" puts "" puts " check:" puts " check the repository for inconsistencies and dangling links" puts "" end def gen_overview() f = File.new("#{$repository_path}/index.html", "w") f.puts "" f.puts "" f.puts " Overview" f.puts "" f.puts "" f.puts "" column = 0 f.puts "" $repository.each { |i| entry = $repository.get(i) if not entry.autogenerated then f.puts "" column += 1 if column >= 6 then column = 0 f.puts "" end end } f.puts "
" if (entry.thumbnails.empty?) f.puts "#{entry.print_name}" else f.puts "" end f.puts "
" f.puts $cc_license_html f.puts "" f.puts "" f.puts "" f.close() end def gen_html_cmd(*args) $repository.each { |md5| entry = $repository.get(md5) f = File.new("#{$repository_path}/#{md5}/index.html", "w") f.puts "" f.puts "" f.puts " " + md5 + "" f.puts "" f.puts "" f.puts "" f.puts "[back]
" f.puts "
" f.puts "

#{entry.name}

" f.puts "MD5: #{entry.md5}
" f.puts "Filename: #{entry.filename}
" f.puts "Type: #{entry.type}
" f.puts "Author: #{entry.author}
" f.puts "Creation: #{entry.ctime}
" f.puts "Size: #{entry.size}
" f.puts "Keywords: #{entry.keywords.join(", ")}
" f.puts "[download]
" f.puts "

" f.puts "

" f.puts "

Render

" entry.renders.each { |thumb| f.puts "
" } f.puts "
" f.puts "
" entry.related.each { |related| rel_entry = $repository.get(related) rel_entry.thumbnails.each { |thumb| f.puts "" } } f.puts $cc_license_html f.puts "" f.puts "" f.close() } end def gen_renders_cmd(*args) puts "Generating renders..." renderer = Renderer.new() if args.empty? then $repository else args end.each { |md5| entry = $repository.get(md5) if (not entry) then puts "Error: no entry for #{md5}" elsif (entry.special == "render" || entry.special == "thumbnail" || (not entry.renders.empty?)) then puts "Error: image is already rendered: #{md5}" else res = renderer.render(entry.path + "/=data", entry.type) if (res) then res.each {|data| newentry = $repository.create_entry_from_data(data) newentry.name = "Render of " + if entry.name then entry.name else "" end newentry.filename = "render:" + entry.filename newentry.special = "render" newentry.autogenerated = true newentry.type = "jpg" newentry.parent = entry.md5 entry.render_add(newentry.md5) } else puts "Error: Couldn't render #{md5}" end end } end def gen_thumbnail_cmd(*args) puts "Generating thumbnails..." # Generate thumbnails for the given id's if args.empty? then $repository else args end.each { |md5| entry = $repository.get(md5) if (entry) then if (entry.special == "thumbnail") then print "Error: Can't generate thumb for '%s', its already a thumb." % [md5] elsif (not entry.thumbnails.empty?) puts "Error: Already have thumbnail for #{md5}" elsif (entry.special == "render") then command = "convert -sample '128x128' '%s/=data' 'jpg:/dev/stdout'" % entry.path output = IO.popen(command).read() newentry = $repository.create_entry_from_data(output) newentry.filename = "thumbnail:" + entry.filename newentry.autogenerated = true newentry.special = "thumbnail" newentry.name = "Thumbnail of " + entry.md5 newentry.type = "jpg" newentry.parent = entry.md5 entry.thumbnail_add(newentry.md5) if (entry.parent) then parent_entry = $repository.get(entry.parent) parent_entry.thumbnail_add(newentry.md5) end show_cmd(newentry.md5) else if (entry.renders == nil || entry.renders.empty?) then puts "Generating render for: %s\n" % [entry.md5] gen_renders_cmd(entry.md5) end if (entry.renders && (not entry.renders.empty?)) then entry.renders.each { |i| gen_thumbnail_cmd(i) } else puts "Error: Can't generate thumb for '%s', since I can't render it." % [md5] end end else puts "Error: #{md5} not in database" end } end def add_cmd(*args) recursive = true args.each { |filename| print "Adding '%s'... " % [filename] if File.directory?(filename) then if recursive then Dir.new(filename).each { |i| if (i != "." && i != ".." && i != ".thumbnails" && i != ".xvpics") then # FIXME: Recursion is evil without a filetype if (File.extname(i) == ".blend") or (File.directory?(filename + "/" + i)) then add_cmd(filename + "/" + i) else puts "Ignoring: #{i}" end end } else puts "error, '%s' is a directory" % filename end else entry = $repository.create_entry_from_file(filename) print "done => %s\n" % [entry.md5] end } end def get_cmd(*args) args.each { |md5| entry = $repository.get(md5) if not entry then puts "Entry '%s' not in the repository" % [md5] else print entry.data() end } end def list_cmd(*args) $repository.each { |i| show_cmd(i) } end def check_cmd(*rest) errors = 0 $repository.each { |i| entry = $repository.get(i) if not entry.check() then print "Error: entry '%s' doesn't validate" % i errors += 1 end } puts "Validation done, %s errors" % errors end def propshow_cmd(md5, property) entrypath = $repository_path + "/" + md5 if File.exist?(entrypath + "/" + "=" + property) then print File.new(entrypath + "/" + "=" + property, "r").read() else puts "Error: property '%s' not available at entry '%s'" % [property, entrypath] end end def propset_cmd(md5, property, value) case property when "name", "keywords", "type", "related" entrypath = $repository_path + "/" + md5 f = File.new(entrypath + "/" + "=" + property, "w") f.write(value) f.close() else puts "Error unknown property type '%s'\n" % [property] end end def search_cmd(prop, *regexs) regexs.each { |regex| re = Regexp.new(regex) # Potentially quite slow... Dir.new($repository_path).each { |i| if (i != "." && i != "..") entry = $repository.get(i) match = case prop when "type" re.match(entry.type) when "name" re.match(entry.name) when "filename" re.match(entry.filename) else raise "Couldn't detect prop type" end if match then puts entry.md5 end end } } end def make_related_cmd(*args_unknown) args = args_unknown.map { |i| if File.exist?(i) then Digest::MD5.hexdigest(File.new(i).read()) else i end } puts "Related: #{args.join(", ")}" args.each { |md5| entry = $repository.get(md5) args.each { |md5| entry.related_add(md5) } } end def clean_cmd(*args) $repository.each { |md5| entry = $repository.get(md5) thumb = entry.thumbnails thumb.delete_if { |md5| not $repository.has(md5) } entry.thumbnails = thumb renders = entry.renders renders.delete_if { |md5| not $repository.has(md5) } entry.renders = renders } end def show_cmd(*args) args.each { |md5| begin entry = $repository.get(md5) if (entry) then print "Identifer: %s\n" % [md5] print "Realpath: %s\n" % (entry.path + "/" + "=data") print "Name: %s\n" % [entry.name] print "Filename: %s\n" % [entry.filename] print "Creation Date: %s\n" % [entry.ctime] print "Type: %s\n" % [entry.type] print "Special: %s\n" % [entry.special] print "Size: %s bytes\n" % [entry.size] print "Keywords: %s\n" % [entry.keywords.join(", ")] print "Renders: %s\n" % [entry.renders.join("\n ")] print "Related: %s\n" % [entry.related.join("\n ")] print "Thumbnails: %s\n" % [entry.thumbnails.join("\n ")] print "\n" else puts "Entry '%s' not in the repository" % [md5] end rescue Errno::ENOENT puts "Entry '%s' not in the repository" % [md5] end } end # main function $repository_path = "/tmp/testrepo" $repository = Repository.new($repository_path) if ARGV.length == 0 then puts "Usage: mediarepo COMMAND [ARGS]" exit() else rest = ARGV.slice(1, ARGV.length()) case ARGV[0] when "add" add_cmd(*rest) when "get" get_cmd(*rest) when "check" check_cmd(*rest) when "list" list_cmd(*rest) when "show" show_cmd(*rest) when "propshow" propshow_cmd(*rest) when "propset" propset_cmd(*rest) when "genthumb" gen_thumbnail_cmd(*rest) when "genrenders" gen_renders_cmd(*rest) when "genoverview" gen_overview() when "search" search_cmd(*rest) when "genhtml" gen_html_cmd() when "makerelated" make_related_cmd(*rest) when "clean" clean_cmd(*rest) when "help" print_help() exit() else puts "Error: Unknown command '%s'" % [ARGV[0]] end end # EOF #