summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xneocitiesfs.pl238
1 files 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 <https://www.gnu.org/licenses/>.
# 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;
- }
- else {
- e_write($path, e_read($path,0,0), 0, 0);
- truncate $files{$dirs}{$file}{fh}, $length;
- my $res = e_flush($path);
- return $res;
+ if (! $files{$dirs}{$file}{fn}) {
+ e_read($path);
}
- 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) = "";