-
-
Notifications
You must be signed in to change notification settings - Fork 160
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement upload API #1085
Implement upload API #1085
Changes from all commits
bc72346
89bc617
b1492b8
03bd9d2
6092acf
f20af34
e0a9f2e
8a23168
28a0b15
ca11644
16e87e2
fe3fe09
c9f6544
19b711a
d86637d
acecc72
74d5576
fed1f5b
f0c50fd
87cf4ae
1a0fd65
6a6f259
ebf2cea
53d3c79
52aa679
64ffcdf
4d22602
4d36f7f
808cf6b
20de021
8d5c79a
a5a72a0
69f49d1
22a9708
13f677f
aa2510c
a707fc4
57077b7
d626832
27ae2eb
0fa9d57
5e3ef51
5376e0e
3c80f53
0fde3d1
389511e
3d1bf1b
228490a
7e93d28
9d1e06e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,21 @@ | ||
package LANraragi::Controller::Api::Archive; | ||
use Mojo::Base 'Mojolicious::Controller'; | ||
|
||
use Digest::SHA qw(sha1_hex); | ||
use Redis; | ||
use Encode; | ||
use Storable; | ||
use Mojo::JSON qw(decode_json); | ||
use Scalar::Util qw(looks_like_number); | ||
|
||
use LANraragi::Utils::Generic qw(render_api_response); | ||
use File::Temp qw(tempdir); | ||
use File::Basename; | ||
use File::Find; | ||
|
||
use LANraragi::Utils::Archive qw(extract_thumbnail); | ||
use LANraragi::Utils::Generic qw(render_api_response is_archive get_bytelength); | ||
use LANraragi::Utils::Database qw(get_archive_json set_isnew); | ||
use LANraragi::Utils::Logging qw(get_logger); | ||
|
||
use LANraragi::Model::Archive; | ||
use LANraragi::Model::Category; | ||
|
@@ -107,6 +114,160 @@ sub serve_file { | |
$self->render_file( filepath => $file ); | ||
} | ||
|
||
# Create a file archive along with any metadata. | ||
# adapted from Upload.pm | ||
sub create_archive { | ||
my $self = shift; | ||
|
||
my $logger = get_logger( "Archive API ", "lanraragi"); | ||
my $redis = LANraragi::Model::Config->get_redis; | ||
|
||
# receive uploaded file | ||
my $upload = $self->req->upload('file'); | ||
my $expected_checksum = $self->req->param('file_checksum'); # optional | ||
|
||
# require file | ||
if ( ! defined $upload || !$upload ) { | ||
return $self->render( | ||
json => { | ||
operation => "upload", | ||
success => 0, | ||
error => "No file attached" | ||
}, | ||
status => 400 | ||
); | ||
} | ||
|
||
# checksum verification stage. | ||
if ( $expected_checksum ) { | ||
my $file_content = $upload->slurp; | ||
my $actual_checksum = sha1_hex($file_content); | ||
if ( $expected_checksum ne $actual_checksum ) { | ||
return $self->render( | ||
json => { | ||
operation => "upload", | ||
success => 0, | ||
error => "Checksum mismatch: expected $expected_checksum, got $actual_checksum." | ||
}, | ||
status => 417 | ||
); | ||
} | ||
} | ||
|
||
my $filename = $upload->filename; | ||
my $uploadMime = $upload->headers->content_type; | ||
$filename = LANraragi::Utils::Database::redis_encode( $filename ); | ||
|
||
# lock resource | ||
my $lock = $redis->setnx( "upload:$filename", 1 ); | ||
if ( !$lock ) { | ||
return $self->render( | ||
json => { | ||
operation => "upload", | ||
success => 0, | ||
error => "Locked resource: $filename." | ||
}, | ||
status => 423 | ||
); | ||
} | ||
$redis->expire( "upload:$filename", 10 ); | ||
|
||
# metadata extraction | ||
my $catid = $self->req->param('category_id'); | ||
my $tags = $self->req->param('tags'); | ||
my $title = $self->req->param('title'); | ||
my $summary = $self->req->param('summary'); | ||
|
||
# return error if archive is not supported. | ||
if ( !is_archive($filename) ) { | ||
$redis->del("upload:$filename"); | ||
$redis->quit(); | ||
return $self->render( | ||
json => { | ||
operation => "upload", | ||
success => 0, | ||
error => "Unsupported file extension ($filename)" | ||
}, | ||
status => 415 | ||
); | ||
} | ||
|
||
# Move file to a temp folder (not the default LRR one) | ||
my $tempdir = tempdir(); | ||
|
||
my ( $fn, $path, $ext ) = fileparse( $filename, qr/\.[^.]*/ ); | ||
my $byte_limit = LANraragi::Model::Config->enable_cryptofs ? 143 : 255; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if this shouldn't be enforced at the Might be one for later tho There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What was the initial purpose of the byte limit? I only kept it there to mirror, I don't know how this will affect temporary file creation if a filename exceeds this limit. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, it's mostly there for synology hardware which has that 255 byte cap, and the notably insane who use cryptoFS on top of that, which limits it down to a further 143 bytes. I believe this would break even temporary filenames if exceeded. |
||
|
||
$filename = $fn; | ||
while ( get_bytelength( $filename . $ext . ".upload") > $byte_limit ) { | ||
$filename = substr( $filename, 0, -1 ); | ||
} | ||
$filename = $filename . $ext; | ||
|
||
my $tempfile = $tempdir . '/' . $filename; | ||
if ( !$upload->move_to($tempfile) ) { | ||
$logger->error("Could not move uploaded file $filename to $tempfile"); | ||
$redis->del("upload:$filename"); | ||
$redis->quit(); | ||
return $self->render( | ||
json => { | ||
operation => "upload", | ||
success => 0, | ||
error => "Couldn't move uploaded file to temporary location." | ||
}, | ||
status => 500 | ||
); | ||
} | ||
|
||
# Update $tempfile to the exact reference created by the host filesystem | ||
# This is done by finding the first (and only) file in $tempdir. | ||
find( | ||
sub { | ||
return if -d $_; | ||
$tempfile = $File::Find::name; | ||
$filename = $_; | ||
}, | ||
$tempdir | ||
); | ||
|
||
my ( $status_code, $id, $response_title, $message ) = LANraragi::Model::Upload::handle_incoming_file( $tempfile, $catid, $tags, $title, $summary ); | ||
|
||
# post-processing thumbnail generation | ||
my %hash = $redis->hgetall($id); | ||
my ( $thumbhash ) = @hash{qw(thumbhash)}; | ||
unless ( length $thumbhash ) { | ||
$logger->info("Thumbnail hash invalid, regenerating."); | ||
my $thumbdir = LANraragi::Model::Config->get_thumbdir; | ||
$thumbhash = ""; | ||
extract_thumbnail( $thumbdir, $id, 0, 1 ); | ||
$thumbhash = $redis->hget( $id, "thumbhash" ); | ||
$thumbhash = LANraragi::Utils::Database::redis_decode($thumbhash); | ||
} | ||
$redis->del("upload:$filename"); | ||
$redis->quit(); | ||
|
||
unless ( $status_code == 200 ) { | ||
return $self->render( | ||
json => { | ||
operation => "upload", | ||
success => 0, | ||
error => $message, | ||
id => $id | ||
}, | ||
status => $status_code | ||
); | ||
} | ||
|
||
return $self->render( | ||
json => { | ||
operation => "upload", | ||
success => 1, | ||
id => $id | ||
}, | ||
status => 200 | ||
); | ||
} | ||
|
||
# Serve an archive page from the temporary folder, using RenderFile. | ||
sub serve_page { | ||
my $self = shift; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't really need this bit if it's already done in
handle_incoming_file
, right?(I'm aware Controller->Upload does the same, but I think I'll eventually deprecate that code path in favor of just using the API in the webclient)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is_archive
ensures that the filename has a valid extension. I don't know how removing this will affect the tempdir logic, I'll need to test it outThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the archive check block is removed, an unhandled exception will be thrown when a filename with an invalid extension or no extension is passed:
which is the is_pdf subroutine... which is really weird
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
huh, I guess is_pdf wasn't written to handle invalid filenames at all and the issue just never popped up - good to know.