#!/usr/bin/ruby -Ku
#CGIとPStoreとERB、基本的な構成
require 'cgi'
require 'pstore'
require 'erb'
require 'digest/md5'
#uploader.rb?
#mode = submit | edit | get_file | delete | rss | [null]
#id = (id指定)
#以下mode=null時のみ有効
#offset = (オフセット量)
#view = (一度にみれる記事の個数)
#クエリ
MODE = 'mode'
OFFSET = 'offset'
VIEW = 'view'
ID = 'id'
DATE = 'date'
#表示初期値
VIEW_DEFAULT = 5
#アップロードサイズ上限, nilで上限なし
FILE_SIZE_LIMIT = 10 * 1024 # byte
#フォーム
FORM_AUTHOR = 'author'
FORM_EMAIL = 'email'
FORM_FILE = 'file'
FORM_MEMO = 'body' #Digest::MD5::digest('body' + Time::now.strftime("%Y%H"))
FORM_DEFAULT = {}
#ファイルの所在
CGI_FILE = (ENV['SCRIPT_NAME'].scan(/\/([^\/]*)/).flatten)[-1]
DATA_DIR = './data'
DB_FILE = "#{DATA_DIR}/uploader.db"
LOCK_FILE = "#{DATA_DIR}/uploader.lock"
ERB_FILE = 'uploader.ehtml'
RSS_FILE = 'uploader.exml'
#CGI設定
DOMAIN = ENV['HTTP_HOST']
PATH = ENV['SCRIPT_NAME']
COOKIE_NAME = 'SimpleUploader'
COOKIE_EXPIRES = 365 * 24 * 60 * 60
#DB内定義
COUNT = 'COUNT'
DATA = 'DATA'
AUTHORIZE = 'AUTHORIZE'
def id2file(id)
return "#{DATA_DIR}/file.#{id}"
end
#投稿
def submit(cgi, id)
params = cgi.params
#データの整理
params.each{|key, value|
value = params[key] = (key != FORM_FILE ? value.first.read : value.first)
if value == (FORM_DEFAULT.include?(key) ? FORM_DEFAULT[key] : '') then
params.delete(key)
end
}
#データチェック
if !params.include?(FORM_AUTHOR) then
raise StandardError.new('Author name required!!')
end
if !params.include?(FORM_EMAIL) then
raise StandardError.new('E-mail address required!!')
end
if !params[FORM_FILE] or (params[FORM_FILE].size == 0) then
raise StandardError.new('No file!!')
end
if FILE_SIZE_LIMIT and params[FORM_FILE].size >= FILE_SIZE_LIMIT
raise StandardError.new("Too big file, limit = #{FILE_SIZE_LIMIT} byte!!")
end
if !params.include?(FORM_MEMO) then
=begin
raise StandardError.new('Body is empty!!')
=end
params[FORM_MEMO] = ''
end
params[FORM_MEMO] = sanitize(params[FORM_MEMO], ['a', 'b', 'i', 'u', 'strong'])
=begin
case params[FORM_MEMO]
when /[\x81-\x9f\xe0-\xef]/
when /(\x8e.|\x8f..|[^\x00-\x7F\x8e\x8f].)/
when /([\xc0-\xcf\xd0-\xdf].|[\xe0-\xef]..)/
else
raise StandardError.new('Internal Server Error!!')
end
=end
#認証キーの設定
params[AUTHORIZE] = parseCookie(cgi.cookies[COOKIE_NAME])[AUTHORIZE]
if !params[AUTHORIZE]
if !id
params[AUTHORIZE] = authorizeKey(params[FORM_AUTHOR], params[FORM_EMAIL])
else
raise StandardError.new('Not found authorize key.')
end
end
#保存
store(params, id)
#転送
cookie = bakeCookie({FORM_AUTHOR => params[FORM_AUTHOR],
FORM_EMAIL => params[FORM_EMAIL],
AUTHORIZE => params[AUTHORIZE]})
cgi.out({'status' => 'REDIRECT',
'Location' => CGI_FILE,
'cookie' => cookie}){"Redirect URL"}
end
#編集
def edit(cgi, id)
get(id).each{|key, value|
cgi.params[key] = value
}
display(cgi, [id], {'edit' => id})
end
#削除
def delete(cgi, id)
PStore.new(DB_FILE).transaction{|db|
if !db.root?(DATA) then db[DATA] = {} end
if !db[DATA].key?(id)
raise StandardError.new('Id is incorrect.')
elsif !cgi.cookies[COOKIE_NAME] then
raise StandardError.new('Not found authorize key.')
elsif db[DATA][id][AUTHORIZE] != parseCookie(cgi.cookies[COOKIE_NAME])[AUTHORIZE]
raise StandardError.new('Authorize key is incorrect.')
else
db[DATA].delete(id)
File::delete(id2file(id))
end
}
cgi.out({'status' => 'REDIRECT',
'Location' => CGI_FILE}){"Redirect URL"}
end
#ファイルの取得
def get_file(cgi, id)
content = nil
file_info = nil
PStore.new(DB_FILE).transaction{|db|
if !db.root?(DATA) then db[DATA] = {} end
if !db[DATA].key?(id)
raise StandardError.new('Id is incorrect.')
else
open(id2file(id), 'r'){|io|
content = io.read
file_info = db[DATA][id][FORM_FILE]
}
end
}
cgi.out({'Content-Type' => file_info[:file_type],
'Content-Length' => file_info[:file_size],
'Content-Disposition' => "attachment; filename=\"#{file_info[:file_name]}\""}){content}
end
#表示
#@param list [(表示するアイテムのid)...]
#@param option {(表示オプション)}
def display(cgi, list = [], option = {})
FORM_DEFAULT.each{|name, value|
if !cgi.params.include?(name) then cgi.params[name] = value end
}
print cgi.header
File.open(ERB_FILE){|fh|
ERB.new(fh.read.untaint).run(binding)
}
end
#RSS出力
def rss(cgi)
list = get.sort.reverse.slice(0, 15)
print cgi.header({"type" => "application/xml"})
File.open(RSS_FILE){|fh|
ERB.new(fh.read.untaint).run(binding)
}
end
#保存、idがnilのとき新規投稿
def store(params, id = nil)
#付加情報
params[DATE] = Time.now
#ファイル情報の抽出
uploaded_file = params[FORM_FILE]
uploaded_file.original_filename =~ %r|([^\\/]+)$|
params[FORM_FILE] = {
:file_name => uploaded_file.original_filename,
:file_type => uploaded_file.content_type,
:file_size => uploaded_file.size,
:file_name_short => $1}
PStore.new(DB_FILE).transaction{|db|
if !id
id = (db.root?(COUNT) ? db[COUNT] : (db[COUNT] = 0)) #id割り当て
else
#認証キー照合
if db[DATA][id][AUTHORIZE] != params[AUTHORIZE] then raise StandardError.new('Authorize key is incorrect.') end
end
params[ID] = id
#データ保存
if !db.root?(DATA) then db[DATA] = {} end
db[DATA][id] = params
db[COUNT] += 1
open(id2file(id), 'w'){|io|
io << uploaded_file.read
}
}
end
#取得
#@param id (アイテムのid)、id = nilの場合は全アイテムidを配列で返す
def get(id = nil)
PStore.new(DB_FILE).transaction{|db|
if !db.root?(DATA) then db[DATA] = {} end
if !id
return db[DATA].keys
elsif db[DATA].key?(id)
return db[DATA][id]
end
}
return nil
end
#認証キー
def authorizeKey(author, email)
PStore.new(DB_FILE).transaction{|db|
if !db.root?(AUTHORIZE) then db[AUTHORIZE] = {} end
if db[AUTHORIZE].include?(email)
return db[AUTHORIZE][email]
else
return db[AUTHORIZE][email] = author.crypt(email)
end
}
end
#クッキーを焼く
def bakeCookie(values = {})
serialize = []
values.each{|key, value|
serialize << (key + '=' + value)
}
return CGI::Cookie.new({'name' => COOKIE_NAME,
'value' => serialize,
'domain' => DOMAIN,
'path' => PATH,
'expires' => Time.now + COOKIE_EXPIRES})
end
#クッキーを解析
def parseCookie(cookie = [])
parsed = {}
cookie.each{|value|
if value =~ /([^=]*)=(.*)/ then parsed[$1] = $2 end
}
return parsed
end
#HTML無毒化
#@param except [(例外事項、ここに指定したタグはデフォルトの指定と逆の動作)]
#@param accept (デフォルトでタグを受け付けるか)
def sanitize(html, except = [], accept = false)
return html.gsub(/<\/?(\w*)\s*[^>]*>/){|matched|
except.include?($1) ? (accept ? '' : $&) : (accept ? $& : '')
}
end
#URLのパース
def query2hash(query)
result = {}
if query
query.scan(/([^=]+)=([^&]+)&?/){|key, value|
result[key] = value
}
end
return result
end
#ロックファイル
def lock fname
begin
begin
f = File.open(fname, "r+")
rescue
f = File.open(fname, "w+")
end
f.flock(File::LOCK_EX)
yield
ensure
f.flock(File::LOCK_UN)
f.close
end
end
#メインルーチン
#lock(LOCK_FILE){
cgi = CGI.new
#クエリの処理
query = query2hash(ENV['QUERY_STRING'])
[ID, OFFSET, VIEW].each{|key|
if query[key] then query[key] = query[key].to_i end
}
begin
case query[MODE]
when 'submit'
submit(cgi, query[ID])
when 'edit'
edit(cgi, query[ID])
when 'get_file'
get_file(cgi, query[ID])
when 'delete'
delete(cgi, query[ID])
when 'rss'
rss(cgi)
else
if query[ID]
display(cgi, [query[ID]], {'form' => 'off'})
else
option = {}
option[VIEW] = view = (query[VIEW] ? query[VIEW] : VIEW_DEFAULT)
offset = (query[OFFSET] ? query[OFFSET] : 0)
if offset > 0
option['before'] = (offset - view > 0 ? offset - view : 0)
end
if offset + view < get.size
option['after'] = offset + view
end
display(cgi, get.sort.reverse.slice(offset, view), option)
end
end
rescue
display(cgi, [], {'error' => $!, 'error_detail' => $@})
end
#}