From 6d0ff4d4822b62118aa806aba48f89a42a43129c Mon Sep 17 00:00:00 2001 From: jake Date: Mon, 6 Nov 2023 02:52:42 -0500 Subject: [PATCH] add caching (to a temp dir) and add relevent code. use API key code over password. remove unneeded comments/clarifed some comments and EOL whitespace --- neocitiesfs.pl | 238 ++++++++++++++++++++++--------------------------- 1 file changed, 108 insertions(+), 130 deletions(-) diff --git a/neocitiesfs.pl b/neocitiesfs.pl index e0e8b53..b4340cb 100755 --- a/neocitiesfs.pl +++ b/neocitiesfs.pl @@ -3,31 +3,29 @@ # the GNU General Public License as published by the Free Software Foundation, either version # 3 of the License, or (at your option) any later version. # -# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along with this program. # If not, see . # author: jake [a\ jakes-mail dot top -# release: 29, Oct 2023 -# -# SETATTR -> truncate, utime, chown, chmod ? -# +# release: 29, Oct 2023 + use strict; use warnings; use 5.010; use Mojo::UserAgent; use JSON; use Fuse qw(fuse_get_context); -use POSIX qw(ENOENT EISDIR EINVAL EEXIST ENOTEMPTY EACCES EFBIG +use POSIX qw(ENOENT EISDIR EINVAL EEXIST ENOTEMPTY EACCES EFBIG EPERM EBADF ENOSPC EMFILE ENOSYS); -use File::Slurper qw(read_text); +use File::Slurper qw(read_text read_binary write_binary); use Mojo::Date; use Getopt::Long; use Carp::Always; use Smart::Comments; -use File::Temp qw(tempfile); +use File::Temp qw(tempfile tempdir); use threads; use threads::shared; @@ -37,7 +35,7 @@ my $api; my $mountpoint; my $config; GetOptions ("user=s" => \$user, - "pass=s" => \$pass, + "pass=s" => \$pass, "api=s" => \$api, "mountpoint=s" => \$mountpoint, "config=s" => \$config, @@ -52,7 +50,7 @@ or die("Error in command line arguments\n"); my $config_ref = from_json(read_text($config)); if (exists $config_ref->{user}) { $user = $config_ref->{user} if not $user; - } + } if (exists $config_ref->{pass}) { $pass = $config_ref->{pass} if not $pass; } @@ -87,6 +85,16 @@ my $suppress_list_update = 0; my $TYPE_DIR = 0040; my $TYPE_FILE = 0100; +my $tmpdir = tempdir(); + +END { + for my $dir (keys %files) { + for my $file (keys %{ $files{$dir} }) { + unlink $files{$dir}{$file}{fn} if $files{$dir}{$file}{fn}; + } + } + rmdir $tmpdir; +} die unless try_auth_info(); get_listing_from_neocities(); @@ -99,13 +107,26 @@ sub try_auth_info { $res = $tx->res; } else { - my $tx = $ua->build_tx(GET => 'https://neocities.org/api/info'); + my $tx = $ua->build_tx(GET => 'https://neocities.org/api/info'); $tx->req->headers->authorization("Bearer $api"); $res = $ua->start($tx)->res; } if ($res->is_error) { die "auth pass or api seems to be incorrect."; } + else { + # get api key and use that over username + password + # checking if API key is valid is wayyyyy quicker than + # checking if user 'username' exists and hashing the supplied + # password then comparing hashes. + if (! $api) { + my $tx = $ua->get("https://$user:$pass\@neocities.org/api/key"); + my $res = $tx->res; + my $body = from_json($res->body); + $api = $body->{api_key}; + undef $pass; + } + } return 1; } @@ -114,18 +135,10 @@ sub get_listing_from_neocities { my $ua = Mojo::UserAgent->new; $ua = $ua->max_redirects(1); # just in case - my ($tx, $res); + my $tx = $ua->build_tx(GET => 'https://neocities.org/api/list'); + $tx->req->headers->authorization("Bearer $api"); + my $res = $ua->start($tx)->res; - if ($pass) { - $tx = $ua->get("https://$user:$pass\@neocities.org/api/list"); - $res = $tx->res; - } - else { - my $tx = $ua->build_tx(GET => 'https://neocities.org/api/list'); - $tx->req->headers->authorization("Bearer $api"); - $res = $ua->start($tx)->res; - } - if ($res->is_success) { my $known_files = from_json($res->body); update_known_files($known_files); @@ -138,6 +151,19 @@ sub get_listing_from_neocities { sub update_known_files { my ($known_files) = @_; + + my %fns; + for my $dirs (keys %files) { + for my $file (keys %{ $files{$dirs} }) { + $fns{$dirs}{$file}{fn} = undef; + # autovivication on shared variables can fatally terminate this program + if (exists $files{$dirs} and exists $files{$dirs} + and exists $files{$dirs}{$file} and exists $files{$dirs}{$file}{fn}) + { + $fns{$dirs}{$file}{fn} = $files{$dirs}{$file}{fn}; + } + } + } undef %files; for my $e (@{ $known_files->{files} }) { @@ -174,8 +200,8 @@ sub update_known_files { mode => $mode, ctime => $times, size => $size, - fn => undef, }); + $files{$dirs}{$filename}{fn} = $fns{$dirs}{$filename}{fn}; } $files{'/'}{'.'} = shared_clone({ type => $TYPE_DIR, @@ -240,10 +266,6 @@ sub e_open { my ($flags, $fileinfo) = @_; return -ENOENT() unless exists($files{$dirs}{$file}); return -EISDIR() if $files{$dirs}{$file}{type} & 0040; - - #my $fh = [ rand() ]; - - #return (0, $fh); return 0; } @@ -258,21 +280,32 @@ sub e_create { sub e_read { my ($dirs, $file) = get_path_and_file(shift); - my ($buf, $off, $fh) = @_; + my ($buf, $off, $_fh) = @_; return -ENOENT() unless exists($files{$dirs}{$file}); - my $ua = Mojo::UserAgent->new; - $ua = $ua->max_redirects(1); # for some reason neocities redirects .html files to not-.html files. - my $res = $ua->get("https://$user.neocities.org/$dirs/$file")->result; - if ($res->is_success) { - return substr($res->body,$off,$buf); + if (! $files{$dirs}{$file}{fn}) { + my $ua = Mojo::UserAgent->new; + $ua = $ua->max_redirects(1); # for some reason neocities redirects .html files to not-.html files. + my $res = $ua->get("https://$user.neocities.org/$dirs/$file")->result; + if ($res->is_success) { + # filehandles CANNOT be shared between threads + (undef, $files{$dirs}{$file}{fn}) = tempfile('neocitiesfs_XXXXXXX', DIR => $tmpdir, UNLINK => 0); + my $fn = $files{$dirs}{$file}{fn}; + # this is what write_binary() does but I had issues with it + open my $fh, '>:raw', $fn; + print $fh $res->body; + close $fh; + + return substr($res->body,$off,$buf); + } + else { + return -77; # EBADFD, file descrpitor in bad state + } } else { - return -77; # EBADFD, file descrpitor in bad state + my $body = read_binary($files{$dirs}{$file}{fn}); + return substr($body, $off, $buf); } - - return -EINVAL() if $off > length($files{$dirs}{$file}->{cont}); - return 0 if $off == length($files{$dirs}{$file}->{cont}); } sub e_statfs { return 255, 1, 1, 1, 1, 2 } @@ -280,37 +313,35 @@ sub e_statfs { return 255, 1, 1, 1, 1, 2 } sub e_write { my ($dirs, $file) = get_path_and_file(shift); my ($buf, $off, $_fh) = @_; - ## # e_write - ## # $off - ## # $fh return -ENOENT() unless exists($files{$dirs}{$file}); if (! $files{$dirs}{$file}{fn}) { # filehandles CANNOT be shared between threads - (undef, $files{$dirs}{$file}{fn}) = tempfile(); + (undef, $files{$dirs}{$file}{fn}) = tempfile('neocitiesfs_XXXXXXX', DIR => $tmpdir); } open my $fh, '>>', $files{$dirs}{$file}{fn}; $fh->autoflush( 1 ); # perl doesnt 'print line' until it sees "\n" normally seek $fh, $off, 0 if $off; print $fh $buf; close $fh; + $files{$dirs}{$file}{modified} = 1; -# my $res = write_to_neocities($dirs, $file, $buf); return length $buf; } sub e_flush { my ($path, $_fh) = @_; my ($dirs, $file) = get_path_and_file($path); - if ($files{$dirs}{$file}{fn}) { + if ($files{$dirs}{$file}{modified} and $files{$dirs}{$file}{modified} == 1) { my $fn = $files{$dirs}{$file}{fn}; my $res = write_to_neocities($dirs, $file, $fn, 1); - unlink $files{$dirs}{$file}{fn}; - delete $files{$dirs}{$file}{fn}; - return res_errno($res, 0); + my $errno = res_errno($res, 0); + if ($errno == 0) { + $files{$dirs}{$file}{modified} = 0; # synchronized so no longer modified + } + return $errno; } else { - # ? return 0; } } @@ -324,18 +355,15 @@ sub e_truncate { my ($dirs, $file) = get_path_and_file($path); return -ENOENT if ! exists $files{$dirs}{$file}; - if ($length == 0) { # truncate entire file - e_write($path, ''); - my $res = e_flush($path); - return $res; + if (! $files{$dirs}{$file}{fn}) { + e_read($path); } - else { - e_write($path, e_read($path,0,0), 0, 0); - truncate $files{$dirs}{$file}{fh}, $length; - my $res = e_flush($path); - return $res; - } - return 0; + open my $fh, '>', $files{$dirs}{$file}{fn}; + truncate $fh, $length; + $files{$dirs}{$file}{modified} = 1; + close $fh; + my $res = e_flush($path); + return $res; } sub e_mknod { @@ -349,19 +377,11 @@ sub e_mknod { sub e_unlink { my ($dirs, $file) = get_path_and_file(shift); my $ua = Mojo::UserAgent->new; - my ($tx, $res); - if ($pass) { - $tx = $ua->post("https://$user:$pass\@neocities.org/api/delete", => {Accept => '*/*'} => form => - {'filenames[]' => [ "$dirs/$file" ]}); - $res = $tx->res; - } - else { - my $tx = $ua->build_tx(POST => 'https://neocities.org/api/delete', => {Accept => '*/*'} => form => - {'filenames[]' => [ "$dirs/$file" ]}); - $tx->req->headers->authorization("Bearer $api"); - $res = $ua->start($tx)->res; - } + my $tx = $ua->build_tx(POST => 'https://neocities.org/api/delete', => {Accept => '*/*'} => form => + {'filenames[]' => [ "$dirs/$file" ]}); + $tx->req->headers->authorization("Bearer $api"); + my $res = $ua->start($tx)->res; $suppress_list_update = 1; my $errno = res_errno($res, 0); @@ -373,23 +393,7 @@ sub e_unlink { } sub e_mkdir { - # so, neocities API doesn't exactly have a '/api/create_dir/' - # BUT does create a dir if you upload a file that is in a dir. - # :) -# my ($dirs, $file) = get_path_and_file(shift); -# return -EEXIST if exists $files{$dirs}{$file}; -# my $numb = int(rand(99999999)) . 'mkdir_hopefully_no_collsions.html'; -# -# $suppress_list_update = 1; -# $res = e_mknod("$dirs/$file/$numb"); -# -# $suppress_list_update = 0; -# return res_errno($res,0) if $res != 0; - -# return e_unlink("$dirs/$file/$numb"); - - # or I could just create a directory 'locally' since it is likely the user will put something in it - # (also reduces calls to /api/) + # making it 'locally' as neocities auto-mkdir when user puts a file my $path = shift; my ($dirs, $file) = get_path_and_file($path); return -EEXIST if exists $files{$dirs}{$file}; @@ -412,7 +416,6 @@ sub e_mkdir { ctime => time(), size => 4096, }; - # ## %files return 0; } @@ -421,24 +424,13 @@ sub e_mkdir { sub e_rmdir { my $path = shift; return -ENOENT if not exists $files{$path}; - # commented out for now; causes too many unlink() and get_listing_from_neocities() which just by themselves take a while to complete - #if (not scalar keys %{ $files{$path} } == 2) { # '.' and '..' - # return -ENOTEMPTY; - #} my $ua = Mojo::UserAgent->new; - my ($tx, $res); - if ($pass) { - $tx = $ua->post("https://$user:$pass\@neocities.org/api/delete", => {Accept => '*/*'} => form => - {'filenames[]' => [ "$path" ]}); - $res = $tx->res; - } - else { - $tx = $ua->build_tx(POST => 'https://neocities.org/api/delete', => {Accept => '*/*'} => form => - {'filenames[]' => [ "$path" ]}); - $tx->req->headers->authorization("Bearer $api"); - $res = $ua->start($tx)->res; - } + my $tx = $ua->build_tx(POST => 'https://neocities.org/api/delete', => {Accept => '*/*'} => form => + {'filenames[]' => [ "$path" ]}); + $tx->req->headers->authorization("Bearer $api"); + my $res = $ua->start($tx)->res; + return res_errno($res, 0); } @@ -451,18 +443,11 @@ sub e_rename { return -EEXIST if exists $files{$new_dirs}{$new_file}; my $ua = Mojo::UserAgent->new; - my ($tx, $res); - if ($pass) { - $tx = $ua->post("https://$user:$pass\@neocities.org/api/rename", => {Accept => '*/*'} => form => - { path => $old_path, new_path => $new_path }); - $res = $tx->res; - } - else { - $tx = $ua->build_tx(POST => 'https://neocities.org/api/rename', => {Accept => '*/*'} => form => - { path => $old_path, new_path => $new_path }); - $tx->req->headers->authorization("Bearer $api"); - $res = $ua->start($tx)->res; - } + my $tx = $ua->build_tx(POST => 'https://neocities.org/api/rename', => {Accept => '*/*'} => form => + { path => $old_path, new_path => $new_path }); + $tx->req->headers->authorization("Bearer $api"); + my $res = $ua->start($tx)->res; + return res_errno($res, 0); } @@ -507,7 +492,7 @@ sub write_to_neocities { defined $is_buf_fn or $is_buf_fn = 0; my $ua = Mojo::UserAgent->new(); - my $asset; + my $asset; if (! $is_buf_fn) { $asset = Mojo::Asset::Memory->new->add_chunk($buffer); } @@ -515,22 +500,15 @@ sub write_to_neocities { $asset = Mojo::Asset::File->new(path => $buffer); } - my ($tx, $res); - if ($pass) { - $tx = $ua->post("https://$user:$pass\@neocities.org/api/upload" => - {Accept => '*/*'} => form => {"$dirs/$file" => { file => $asset } }); - $res = $tx->res; - } - else { - $tx = $ua->build_tx(POST => 'https://neocities.org/api/upload' => - {Accept => '*/*'} => form => {"$dirs/$file" => { file => $asset } }); - $tx->req->headers->authorization("Bearer $api"); - $res = $ua->start($tx)->res; - } + my $tx = $ua->build_tx(POST => 'https://neocities.org/api/upload' => + {Accept => '*/*'} => form => {"$dirs/$file" => { file => $asset } }); + $tx->req->headers->authorization("Bearer $api"); + my $res = $ua->start($tx)->res; undef $asset; return $res; } + # If you run the script directly, it will run fusermount, which will in turn # re-run this script. Hence the funky semantics. #my ($mountpoint) = "";