Created
October 6, 2013 10:30
-
-
Save nihen/6852251 to your computer and use it in GitHub Desktop.
これとpublicファイルがあればIsucon3アプリの完成です。
Plack ServerはFeersumで、front httpdはnginxでした。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use 5.14.0; | |
use utf8; | |
use Plack::Request; | |
use File::Basename; | |
use IO::Handle; | |
use Encode; | |
use List::Util qw/sum/; | |
use POSIX qw/strftime/; | |
use JSON::XS; | |
use Plack::Session::State::Cookie; | |
use Digest::SHA qw/ sha256_hex /; | |
use Plack::Util; | |
use Text::Markdown::Hoedown qw/markdown/; | |
use Text::Xslate qw/html_escape/; | |
use DBIx::Sunny; | |
my $header = ['content-type' => 'text/html']; | |
my $memos = []; | |
my $memos_public = []; | |
my $memos_by_user = +{}; | |
my $memos_by_user_public = +{}; | |
my $users = +{}; | |
my $uri_base = 'http://localhost'; | |
my $user_log_file = '/home/isucon/data/user_logfile'; | |
my $user_log; | |
my $memo_log_file = '/home/isucon/data/memo_logfile'; | |
my $memo_log; | |
our $log_read_mode = 0; | |
{ | |
# preload | |
local $log_read_mode = 1; | |
if ( -e $user_log_file ) { | |
open $user_log, '<', $user_log_file; | |
my @lines = <$user_log>; | |
$user_log->close; | |
for my $line (@lines) { | |
chomp $line; | |
my $user = decode_json($line); | |
_create_user_old($user); | |
} | |
open $user_log, '>>', $user_log_file; | |
} | |
else { | |
open $user_log, '>', $user_log_file; | |
} | |
if ( -e $memo_log_file ) { | |
open $memo_log, '<', $memo_log_file; | |
my @lines = <$memo_log>; | |
$memo_log->close; | |
for my $line (@lines) { | |
chomp $line; | |
my $memo = decode_json($line); | |
_post_memo(+{ | |
user => $users->{$memo->{username}}, | |
content => $memo->{content}, | |
is_private => $memo->{is_private}, | |
created_at => $memo->{created_at}, | |
}); | |
} | |
open $memo_log, '>>', $memo_log_file; | |
} | |
else { | |
open $memo_log, '>', $memo_log_file; | |
} | |
} | |
sub init { | |
close $user_log; | |
close $memo_log; | |
open $user_log, '>', $user_log_file; | |
open $memo_log, '>', $memo_log_file; | |
$memos = []; | |
$memos_public = []; | |
$memos_by_user = +{}; | |
$memos_by_user_public = +{}; | |
$users = +{}; | |
my $dbh = DBIx::Sunny->connect( | |
"dbi:mysql:database=isucon;host=localhost;port=3306", 'isucon', '', { | |
RaiseError => 1, | |
PrintError => 0, | |
AutoInactiveDestroy => 1, | |
mysql_enable_utf8 => 1, | |
mysql_auto_reconnect => 1, | |
}, | |
); | |
my $init_users = $dbh->select_all("SELECT username, password, salt, last_access FROM users"); | |
for my $user ( @{$init_users} ) { | |
_create_user_old($user); | |
} | |
my $init_memos = $dbh->select_all(q{ | |
SELECT content, is_private, memos.created_at, username | |
FROM memos join users on (users.id = memos.user) ORDER BY created_at asc | |
}); | |
for my $memo ( @{$init_memos} ) { | |
_post_memo(+{ | |
user => $users->{$memo->{username}}, | |
content => $memo->{content}, | |
is_private => $memo->{is_private}, | |
created_at => $memo->{created_at}, | |
}); | |
} | |
} | |
if ( 0 ) { | |
# for test | |
my $chiba = _create_user('chiba', 'chiba'); | |
my $tester = _create_user('test', 'test'); | |
my $isucon1 = _create_user('isucon1', 'isucon1'); | |
my $test_content = q{ | |
A First Level Header | |
==================== | |
A Second Level Header | |
--------------------- | |
Now is the time for all good men to come to | |
the aid of their country. This is just a | |
regular paragraph. | |
The quick brown fox jumped over the lazy | |
dog's back. | |
### Header 3 | |
> This is a blockquote. | |
> | |
> This is the second paragraph in the blockquote. | |
> | |
> ## This is an H2 in a blockquote | |
}; | |
for my $id ( 1..500 ) { | |
_post_memo(+{ | |
user => $isucon1, | |
content => sprintf("test %s\ndesu\n%s",$id, $test_content), | |
is_private => $id % 3 == 0 ? 1 : 0, | |
}); | |
} | |
} | |
my $mysessionstore = +{}; | |
package MySessionStore { | |
use strict; | |
use warnings; | |
our $VERSION = '0.01'; | |
use parent 'Plack::Session::Store'; | |
use Plack::Util::Accessor qw[ prefix expires ]; | |
sub new { | |
my ($class, %params) = @_; | |
bless { %params } => $class; | |
} | |
sub fetch { | |
my ($self, $session_id ) = @_; | |
$mysessionstore->{$session_id} | |
} | |
sub store { | |
my ($self, $session_id, $session) = @_; | |
$mysessionstore->{$session_id} = $session; | |
} | |
sub remove { | |
my ($self, $session_id) = @_; | |
delete $mysessionstore->{$session_id}; | |
} | |
1; | |
} | |
sub notfound() {q{<!doctype html> | |
<html> | |
<head> | |
<meta charset=utf-8 /> | |
<style type="text/css"> | |
.message { | |
font-size: 200%; | |
margin: 20px 20px; | |
color: #666; | |
} | |
.message strong { | |
font-size: 250%; | |
font-weight: bold; | |
color: #333; | |
} | |
</style> | |
</head> | |
<body> | |
<p class="message"> | |
<strong>404</strong> Not Found | |
</p> | |
</div> | |
</body> | |
</html>}} | |
sub base_top() {qq{<!DOCTYPE html> | |
<html> | |
<head> | |
<meta http-equiv="Content-Type" content="text/html" charset="utf-8"> | |
<title>Isucon3</title> | |
<link rel="stylesheet" href="${uri_base}/css/bootstrap.min.css"> | |
<style> | |
body { | |
padding-top: 60px; | |
} | |
</style> | |
<link rel="stylesheet" href="${uri_base}/css/bootstrap-responsive.min.css"> | |
<link rel="stylesheet" href="${uri_base}/"> | |
</head> | |
<body> | |
<div class="navbar navbar-fixed-top"> | |
<div class="navbar-inner"> | |
<div class="container"> | |
<a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse"> | |
<span class="icon-bar"></span> | |
<span class="icon-bar"></span> | |
<span class="icon-bar"></span> | |
</a> | |
<a class="brand" href="${uri_base}/">Isucon3</a> | |
<div class="nav-collapse"> | |
<ul class="nav"> | |
<li><a href="${uri_base}/">Home</a></li> | |
}} | |
sub header { | |
my $env = shift; | |
my @body = (); | |
my $user = $env->{user}; | |
if ( $user ) { | |
push @body, sprintf( | |
qq{<li><a href="${uri_base}/mypage">MyPage</a></li><li> | |
<form action="${uri_base}/signout" method="post"> | |
<input type="hidden" name="sid" value="%s"> | |
<input type="submit" value="SignOut"> | |
</form> | |
</li>}. "\n", | |
$env->{"psgix.session"}{token} | |
); | |
} | |
else { | |
push @body, qq{<li><a href="${uri_base}/signin">SignIn</a></li>} . "\n"; | |
} | |
return @body, sprintf( | |
q{</ul></div> <!--/.nav-collapse --></div></div></div><div class="container"><h2>Hello %s!</h2>}, | |
$user ? html_escape($user->{username}) : '' | |
); | |
} | |
sub base_bottom() {qq{</div> <!-- /container --> | |
<script type="text/javascript" src="${uri_base}/js/jquery.min.js"></script> | |
<script type="text/javascript" src="${uri_base}/js/bootstrap.min.js"></script> | |
</body> | |
</html> | |
}} | |
sub content_index { | |
my $page = shift; | |
my $total = @{$memos_public}; | |
my @body = (sprintf( | |
q{<h3>public memos</h3><p id="pager"> recent %s - %s / total <span id="total">%s</span></p><ul id="memos">}, | |
$page * 100 + 1, | |
$page * 100 + 100, | |
$total | |
)); | |
for my $index (($page * 100)..($page*100+99)) { | |
last if $total < $index+1; | |
my $memo = $memos_public->[$total-$index-1]; | |
push @body, sprintf( | |
qq{<li><a href="${uri_base}/memo/%s">%s</a> by %s (%s)</li>\n}, | |
$memo->{id}, | |
html_escape($memo->{title}), | |
html_escape($memo->{user}{username}), | |
$memo->{created_at}, | |
); | |
} | |
return @body, '</ul>'; | |
} | |
sub mypage { | |
my $env = shift; | |
my $user = $env->{user}; | |
my @body = (sprintf(qq{<form action="${uri_base}/memo" method="post"> | |
<input type="hidden" name="sid" value="%s"> | |
<textarea name="content"></textarea> | |
<br> | |
<input type="checkbox" name="is_private" value="1"> private | |
<input type="submit" value="post"> | |
</form> | |
<h3>my memos</h3> | |
<ul>}, | |
$env->{"psgix.session"}{token} | |
)); | |
my $user_memos = $memos_by_user->{$user->{username}} //= []; | |
for my $memo (reverse @{$user_memos}) { | |
push @body, sprintf( | |
qq{<li><a href="${uri_base}/memo/%s">%s</a> by %s (%s)%s</li>\n}, | |
$memo->{id}, | |
html_escape($memo->{title}), | |
html_escape($memo->{username}), | |
$memo->{created_at}, | |
$memo->{is_private} ? '[private]' : '', | |
); | |
} | |
return @body, '</ul>'; | |
} | |
sub get_signin() {qq{<form action="${uri_base}/signin" method="post"> | |
username <input type="text" name="username" size="20"> | |
<br> | |
password <input type="password" name="password" size="20"> | |
<br> | |
<input type="submit" value="signin"> | |
</form> | |
}} | |
sub post_signin { | |
my $env = shift; | |
my $param = $env->{param}; | |
my $username = $param->{username}; | |
my $password = $param->{password}; | |
my $user = $users->{$username}; | |
if ( $user && $user->{password} eq sha256_hex($user->{salt} . $password) ) { | |
$env->{"psgix.session.options"}{change_id} = 1; | |
my $session = $env->{"psgix.session"}; | |
$session->{username} = $username; | |
$session->{token} = sha256_hex(rand()); | |
$user->{last_access} = time(); | |
return ['302', [Location => "${uri_base}/mypage"], []]; | |
} | |
else { | |
return ['200', $header, [ | |
base_top(), | |
header($env), | |
get_signin(), | |
base_bottom(), | |
]]; | |
} | |
} | |
sub signout { | |
my $env = shift; | |
$env->{"psgix.session.options"}{change_id} = 1; | |
delete $env->{"psgix.session"}{username}; | |
return ['302', [Location => "${uri_base}/"], []]; | |
} | |
sub _create_user { | |
my ($username, $password) = @_; | |
my $salt = substr( sha256_hex( time() . $username ), 0, 8 ); | |
my $password_hash = sha256_hex( $salt, $password ); | |
my $user = $users->{$username} = +{ | |
username => $username, | |
password => $password_hash, | |
salt => $salt, | |
last_access => undef, | |
}; | |
$user_log->printflush(encode_json($user), "\n") unless $log_read_mode; | |
return $user; | |
} | |
sub _create_user_old { | |
my ($user) = @_; | |
$user_log->printflush(encode_json($user), "\n") unless $log_read_mode; | |
return $users->{$user->{username}} = $user; | |
} | |
sub signup { | |
my $env = shift; | |
my $param = $env->{param}; | |
my $username = $param->{username}; | |
my $password = $param->{password}; | |
my $user = $users->{$username}; | |
if ($user) { | |
return ['400', [], []]; | |
} | |
else { | |
_create_user($username, $password); | |
$env->{"psgix.session"}{username} = $username; | |
return ['302', [Location => "${uri_base}/mypage"], []]; | |
} | |
} | |
sub get_signup() {qq{<form action="${uri_base}/signup" method="post"> | |
username <input type="text" name="username" size="20"> | |
<br> | |
password <input type="password" name="password" size="20"> | |
<br> | |
<input type="submit" value="signup"> | |
</form> | |
}} | |
sub _post_memo { | |
my $memo = shift; | |
$memo->{content_html} //= markdown($memo->{content}); | |
$memo->{title} //= [split /[\r\n]/, $memo->{content}]->[0]; | |
$memo->{created_at} //= strftime('%Y-%m-%d %H:%M:%S', localtime()); | |
my $id = @{$memos} + 1; | |
$memo->{id} = $id; | |
push $memos, $memo; | |
my $user_memos = $memos_by_user->{$memo->{user}{username}} //= []; | |
my $user_memo_id = @{$user_memos} + 1; | |
if ( $user_memo_id > 1 ) { | |
$memo->{older_private} = $user_memos->[$user_memo_id-2]; | |
$memo->{older_private}{newer_private} = $memo; | |
} | |
push $user_memos, $memo; | |
if ( !$memo->{is_private} ) { | |
push $memos_public, $memo; | |
my $user_memos_public = $memos_by_user_public->{$memo->{user}{username}} //= []; | |
my $user_memo_public_id = @{$user_memos_public} + 1; | |
if ( $user_memo_public_id > 1 ) { | |
$memo->{older_public} = $user_memos_public->[$user_memo_public_id-2]; | |
$memo->{older_public}{newer_public} = $memo; | |
} | |
push $user_memos_public, $memo; | |
} | |
$memo_log->printflush(encode_json(+{ | |
username => $memo->{user}{username}, | |
content => $memo->{content}, | |
is_private => $memo->{is_private}, | |
created_at => $memo->{created_at}, | |
}), "\n") unless $log_read_mode; | |
return $id; | |
} | |
sub post_memo { | |
my $env = shift; | |
my $param = $env->{param}; | |
my $id = _post_memo(+{ | |
user => $env->{user}, | |
content => Encode::decode('utf-8', $param->{content}), | |
is_private => $param->{is_private} ? 1 : 0, | |
}); | |
return ['302', [Location => "${uri_base}/memo/" . $id], []]; | |
} | |
sub get_memo { | |
my ($env, $id) = @_; | |
$id=$id+0; | |
if ( !$id || $id > @{$memos} ) { | |
return ['404', [], []]; | |
} | |
my $memo = $memos->[$id-1]; | |
my $user = $env->{user}; | |
if ($memo->{is_private}) { | |
if ( !$user || $user->{username} ne $memo->{user}{username} ) { | |
return ['404', [], []]; | |
} | |
} | |
my @body = (sprintf( | |
q{<p id="author">%s Memo by %s (%s)</p><hr>}, | |
$memo->{is_private} ? 'Private' : 'Public', | |
html_escape($memo->{user}{username}), | |
$memo->{created_at}, | |
)); | |
my $older_memo; | |
my $newer_memo; | |
if ( $user && $user->{username} eq $memo->{user}{username} ) { | |
$older_memo = $memo->{older_private}; | |
$newer_memo = $memo->{newer_private}; | |
} | |
else { | |
$older_memo = $memo->{older_public}; | |
$newer_memo = $memo->{newer_public}; | |
} | |
if ( $older_memo ) { | |
#older | |
push @body, sprintf( | |
qq{<a id="older" href="${uri_base}/memo/%s">< older memo</a>} . "\n", | |
$older_memo->{id}, # old memo id | |
); | |
} | |
push @body, '|' . "\n"; | |
if ( $newer_memo ) { | |
#newr | |
push @body, sprintf( | |
qq{<a id="newer" href="${uri_base}/memo/%s">newer memo ></a>} . "\n", | |
$newer_memo->{id}, # new memo id | |
); | |
} | |
push @body, sprintf(q{<hr><div id="content_html">%s</div>}, $memo->{content_html}); | |
return ['200', $header, [ | |
base_top(), | |
header($env), | |
@body, | |
base_bottom(), | |
]]; | |
} | |
sub app { | |
my $env = shift; | |
my $res = _app($env); | |
if ( $res->[0] == 404 ) { | |
$res->[2] = [notfound()]; | |
} | |
if ( !$env->{no_get_user} && $env->{user} ) { | |
my $myheader = [@{$res->[1]}]; | |
Plack::Util::header_iter(['Cache-Control', 'private'], sub {Plack::Util::header_set($myheader, @_)}); | |
$res->[1] = $myheader; | |
} | |
return $res; | |
} | |
sub _app { | |
my $env = shift; | |
my $method = uc($env->{REQUEST_METHOD}); | |
my $path_info = $env->{PATH_INFO}; | |
$uri_base = 'http://' . $env->{HTTP_HOST}; | |
my $session = $env->{"psgix.session"}; | |
if ( $session && exists $session->{username} && exists $users->{$session->{username}} ) { | |
$env->{user} = $users->{$session->{username}}; | |
} | |
if ( $method eq 'GET' ) { | |
if ( $path_info eq '/' ) { | |
return ['200', $header, [ | |
base_top(), | |
header($env), | |
content_index(0), | |
base_bottom(), | |
]]; | |
} | |
elsif ( $path_info =~ m{\A/memo/(\d+)\z}o ) { | |
return get_memo($env, $1); | |
} | |
elsif ( $path_info =~ m{\A/recent/(\d+)\z}o ) { | |
return ['200', $header, [ | |
base_top(), | |
header($env), | |
content_index($1), | |
base_bottom(), | |
]]; | |
} | |
elsif ( $path_info eq '/mypage' ) { | |
return ['302', [Location => "${uri_base}/"], []] unless $env->{user}; | |
return ['200', $header, [ | |
base_top(), | |
header($env), | |
mypage($env), | |
base_bottom(), | |
]]; | |
} | |
elsif ( $path_info eq '/signin' ) { | |
return ['200', $header, [ | |
base_top(), | |
header($env), | |
get_signin(), | |
base_bottom(), | |
]]; | |
} | |
elsif ( $path_info eq '/signup' ) { | |
return ['200', $header, [ | |
base_top(), | |
header($env), | |
get_signup(), | |
base_bottom(), | |
]]; | |
} | |
elsif ( $path_info eq '/init' ) { | |
init(); | |
return ['200', [], ['OK']]; | |
} | |
} | |
elsif ( $method eq 'POST' ) { | |
$env->{param} = Plack::Request->new($env)->body_parameters; | |
if ( $path_info eq '/signin' ) { | |
$env->{no_get_user} = 1; | |
return post_signin($env); | |
} | |
else { | |
if ( $env->{param}->{sid} ne $env->{"psgix.session"}{token} ) { | |
return ['400', [], []]; | |
} | |
if ( $path_info eq '/memo' ) { | |
return ['302', [Location => "${uri_base}/"], []] unless $env->{user}; | |
return post_memo($env); | |
} | |
elsif ( $path_info eq '/signout' ) { | |
return ['302', [Location => "${uri_base}/"], []] unless $env->{user}; | |
return signout($env); | |
} | |
elsif ( $path_info eq '/signup' ) { | |
$env->{no_get_user} = 1; | |
return signup($env); | |
} | |
} | |
} | |
return ['404', $header, ['not found']]; | |
} | |
my $root_dir = File::Basename::dirname(__FILE__); | |
use Plack::Builder; | |
builder { | |
enable 'ReverseProxy'; | |
# enable 'Static', | |
# path => qr!^/(?:(?:css|js|img)/|favicon\.ico$)!, | |
# root => $root_dir . '/public'; | |
enable 'Session', | |
store => MySessionStore->new(), | |
state => Plack::Session::State::Cookie->new( | |
httponly => 1, | |
session_key => "isucon_session", | |
), | |
; | |
mount '/' => \&app; | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment