class Softcover::Book include Softcover::Utils include Softcover::Output DEFAULT_MEDIA_DIR = "media" attr_accessor :errors, :uploader, :signatures, :manifest, :processed_media, :media_dir class UploadError < StandardError; end def initialize(options={}) require "softcover/client" @manifest = Softcover::BookManifest.new(options) @marketing = Softcover::MarketingManifest.new @client = Softcover::Client.new_with_book self @media_dir = DEFAULT_MEDIA_DIR @processed_media = [] end class BookFile < Struct.new(:path) LAST_WRITE_HORIZON = 0 attr_accessor :checksum def initialize(*args) super @checksum = Digest::MD5.file(path).hexdigest (@@lookup ||= {})[path] = self end def ready? return true if Softcover::test? File::ctime(path).to_i < Time.now.to_i - LAST_WRITE_HORIZON end def to_json(opts={}) { path: path, checksum: @checksum }.to_json end def self.find(path) @@lookup[path] end end # TODO: extract pattern to config helper: # has_config_for :id, :last_uploaded_at, path: ".polytex-book" def id Softcover::BookConfig['id'] end def id=(n) Softcover::BookConfig['id'] = n end # get array of paths and checksums def files paths = %W{html/#{slug}.html html/*_fragment.html images/**/* config/*} Dir[*paths].map do |path| BookFile.new(path) unless File.directory?(path) end.compact end def filenames files.map &:path end def chapter_attributes chapters.map(&:to_hash) end def url # TODO: append api_token to auto-login? "#{@client.host}/books/#{id}/redirect" end # Opens the book in the browser (OS X & Linux). def open_in_browser `#{open} #{url}` end # Returns the system-dependent `open` command. def open if os_x? 'open' elsif linux? 'xdg-open' else raise "Platform #{RUBY_PLATFORM} not supported" end end def create_or_update(options={}) raise "HTML not built!" if Dir['html/*'].empty? params = { id: id, files: files, title: title, slug: slug, subtitle: subtitle, description: description, chapters: chapter_attributes, prices: prices, faq: faq, testimonials: testimonials, marketing_content: marketing_content, contact_email: contact_email, hide_softcover_footer: hide_softcover_footer, authors: authors, ga_account: ga_account, remove_unused_media_bundles: options[:remove_unused_media_bundles] } res = @client.create_or_update_book params if res['errors'] @errors = res['errors'] return false end # is this needed? @attrs = res['book'] self.id = @attrs['id'] # Not needed for now: # Softcover::BookConfig['last_uploaded_at'] = Time.now # res contains the S3 upload signatures needed @uploader = Softcover::Uploader.new res true rescue Exception => e @errors = [e.message] raise e false end def upload!(options={}) @uploader.after_each do |params| notify_file_upload params['path'] end @uploader.upload!(options) notify_upload_complete end def notify_upload_complete res = @client.notify_upload_complete if res['errors'].nil? return url else raise UploadError, "Couldn't verify upload: #{res['errors']}" end end def notify_file_upload(path) book_file = BookFile.find path # this could spin off new thread: @client.notify_file_upload path: book_file.path, checksum: book_file.checksum end def destroy res = @client.destroy if res['errors'] @errors = res['errors'] return false end true end # ============================================================================ # Media handling # ============================================================================ # 1. iterate over /media/* # => use directory name as path parameter # => get checksums for all included files # => send each to /media API endpoint and then upload def process_media(options={}) Dir["media/*"].each do |media_dir| next unless File.directory?(media_dir) && !(media_dir =~ /^\./) process_media_directory media_dir, options end end def process_media_directory(dir, options={}) return false if @processed_media.include?(dir) puts "Processing #{dir} directory..." files_to_upload = get_book_files(dir).select do |file| file.ready? end upload_media! dir, files_to_upload, options @processed_media.push dir end def get_book_files(dir) Dir["#{dir}/**/*"].map do |path| BookFile.new(path) unless File.directory?(path) end.compact end def upload_media!(path, files, options={}) return if files.empty? manifest_path = File.join(path, "manifest.yml") manifest = File.exists?(manifest_path) ? File.read(manifest_path) : nil res = @client.get_media_upload_params path, files, manifest, options if res['upload_params'] media_uploader = Softcover::Uploader.new res media_uploader.after_each do |params| notify_file_upload params['path'] end media_uploader.upload! notify_upload_complete else raise 'server error' end end private def method_missing(name, *args, &block) @manifest.send(name) || @marketing.send(name) || nil end end