#!/usr/bin/ruby

# Copyright (C) 2008, IWAMURO Motonori
# All rights reserved.
#
# License: BSD License (revised)
# see http://vmi.jp/software/ruby/COPYING

### Configuration
UPLOAD_DIR = '../uploaded'
FS_CHARSET = :auto
FS_CHARSET_CYGWIN = 'CP932'
HOME_URI = :auto

### Initialize
require 'cgi'
require 'iconv'

BLOCK_SIZE = 64 * 1024
INVALID_UTF8 = /[\xC0\xC1\xF8-\xFD]|\xE0[\x80-\x9F]|\xF0[\x80-\x8F]/n

### Utilities
def comma(num)
  num.to_s.gsub(/(\d{1,3})(?=\d{3}+(?:$|\D))/, '\1,')
end

def round(num)
  (num * 10.0).round / 10.0
end

### Views
class Uploader < CGI
  HEADER = { 'charset' => 'UTF-8' }
  KBytes = 1024.0
  MBytes = KBytes * 1024.0
  GBytes = MBytes * 1024.0

  def h(s)
    CGI.escapeHTML(s)
  end

  def back_uri
    if HOME_URI != :auto
      HOME_URI
    else
      script_name.chomp('/').sub(/(?:cgi-[^\/]+\/)?[^\/]*$/, '')
    end
  end

  def page_error(status, msg = nil)
    if HTTP_STATUS.has_key?(status)
      status = 'SERVER_ERROR'
    end
    st_msg = HTTP_STATUS[status]
    msg = ' - ' + h(msg) if msg
    out('status' => status) do <<-EOF
<html>
<head>
  <title>#{st_msg}</title>
</head>
<body>
  #{st_msg}#{msg}
</body>
</html>
    EOF
    end
    exit(0)
  end

  def page_form
    out(HEADER) do
      <<-EOF
<html>
<head>
  <title>Tiny Uploader</title>
</head>
<body>
  <h1>Tiny Uploader</h1>
  <hr>
  <p>
  <a href="#{script_name}/list">[List of Files]</a>
  </p>
  <p>Filesystem Charset: #{$charset}</p>
  <form action="#{script_name}"
        method="POST"
        enctype="multipart/form-data">
    File: <input type="file" name="file">
          <input type="submit" value="Upload"><br>
  </form>
  <hr>
  <a href="#{back_uri}">[HOME]</a>
</body>
</html>
      EOF
    end
  end

  def file_tr(name, stat)
    size = stat.size
    case
    when size >= GBytes
      rsize = comma(round(size / GBytes)) + "GB"
    when size >= MBytes
      rsize = comma(round(size / MBytes)) + "MB"
    when size >= KBytes
      rsize = comma(round(size / KBytes)) + "KB"
    else
      rsize = comma(size) + 'B'
    end

    <<-EOF
      <tr>
        <td align="center">
          <input type="checkbox" name="files" value="#{h name}">
        </td>
        <td><a href="#{script_name}/download/#{h name}">#{h name}</a></td>
        <td align="right">#{rsize}</td>
        <td align="right">(#{comma size})</td>
        <td>#{stat.mtime.strftime('%Y-%m-%d %H:%M:%S')}</td>
      </tr>
    EOF
  end

  def page_file_info(title, list, list_link = false)
    if list.length == 0
      tr = '<tr><td colspan="5" align="center">No Files.</td></tr>'
    elsif list[0].instance_of?(Array)
      tr = list.map do |name, stat|
        file_tr(name, stat)
      end.join
    else
      tr = file_tr(*list)
    end
    if list_link
      list_of_files = %'| <a href="#{script_name}/list">[List of Files]</a>'
    else
      list_of_files = ''
    end
    out(HEADER) do
      <<-EOF
<html>
<head>
  <title>#{title}</title>
</head>
<body>
  <h1>#{title}</h1>
  <hr>
  <p>
  <a href="#{script_name}">[Upload Form]</a>
  #{list_of_files}
  </p>
  <form action="#{script_name}/delete" method="POST">
    <table border="1">
      <tr>
        <th>Delete</th>
        <th>File Name</th>
        <th>Size</th>
        <th>Size (full)</th>
        <th>Date</th>
      </tr>
#{tr}
    </table>
    <br>
    <input type="submit" value="Delete Checked Files">
  </form>
  <hr>
  <a href="#{back_uri}">[HOME]</a>
</body>
</html>
      EOF
    end
  end
end

# Main
cgi = Uploader.new
begin
  Dir.chdir(UPLOAD_DIR)

  # Detect charset
  if FS_CHARSET != :auto
    $charset = FS_CHARSET
  elsif /\.([^.]+)$/ =~ ENV['LANG']
    $charset = $1
  elsif /cygwin/ =~ RUBY_PLATFORM
    $charset = FS_CHARSET_CYGWIN
  else
    $charset = 'UTF-8'
  end
  if /UTF[-_]?8/i =~ $charset
    def to_fs(name)
      name
    end
    def from_fs(name)
      name
    end
  else
    $to_fs = Iconv.new($charset, 'UTF-8')
    $from_fs = Iconv.new('UTF-8', $charset)
    def to_fs(name)
      $to_fs.iconv(name)
    end
    def from_fs(name)
      $from_fs.iconv(name)
    end
  end

  # Dispatch
  path_info = cgi.path_info.to_s.chomp('/')
  case cgi.request_method + path_info
  when 'GET'
    cgi.page_form

  when 'GET/list', 'POST/delete'
    if path_info == '/delete'
      cgi.params['files'].each do |name|
        fs_name = to_fs(name)
        begin
          File.delete(fs_name)
        rescue
          # nop
        end
      end
    end
    list = Dir.glob('*').select do |name|
      FileTest.file?(name)
    end.map do |name|
      [from_fs(name), File.stat(name)]
    end.sort {|a, b| b[1].mtime <=> a[1].mtime || a[0] <=> b[0]}
    cgi.page_file_info('List of Files', list)

  when %r{^GET/download/(.*)$}
    name = $1
    fs_name = to_fs(name)
    cgi.print cgi.header('type' => 'application/octet-stream',
                         'length' => FileTest.size(fs_name),
                         'Content-Disposition' => 'attachment')
    open(fs_name, 'r') do |ifh|
      ifh.binmode
      while buffer = ifh.read(BLOCK_SIZE)
        cgi.print buffer
      end
    end

  when 'POST'
    ifh = cgi['file']
    if ifh.nil? || !ifh.respond_to?(:original_filename)
      cgi.page_error('BAD_REQUEST')
    end
    name = ifh.original_filename
    if name.nil? || name.length == 0
      cgi.page_error('BAD_REQUEST')
    end
    if INVALID_UTF8 =~ name
      cgi.page_error('BAD_REQUEST')
    end
    name.sub!(/^.*[\/\\]/, '')
    name.gsub!(/[\x00-\x20!\"\#%&\'()*+,:;<>?@\[\]^_`{|}~\x7f]+/, '_')
    name.sub!(/^_/, '')
    name.sub!(/_$/, '')
    name.gsub!(/_\./, '.')
    fs_name = to_fs(name)
    open(fs_name, 'w') do |ofh|
      ifh.binmode
      ofh.binmode
      while buffer = ifh.read(BLOCK_SIZE)
        ofh.write(buffer)
      end
    end
    stat = File.stat(fs_name)
    cgi.page_file_info("Uploaded - #{name}", [name, stat], true)

  when /POST|GET/
    cgi.page_error('NOT_FOUND')

  else
    cgi.page_error('NOT_IMPLEMENTED')
  end
rescue
  cgi.page_error('SERVER_ERROR', $@)
end
