summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjake <jake@jakes-mail.top>2023-10-29 18:39:27 -0400
committerjake <jake@jakes-mail.top>2023-10-29 18:39:27 -0400
commite091da0412d1ff47c2b55ea33cbce23ef4281507 (patch)
tree1c0e5b0f2d05237147a93b0105bbef0a72cfbca3
GIT THIS
-rw-r--r--.gitignore1
-rw-r--r--config.json.sample5
-rwxr-xr-xneocitiesfs.pl456
3 files changed, 462 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d344ba6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+config.json
diff --git a/config.json.sample b/config.json.sample
new file mode 100644
index 0000000..3bbc0f9
--- /dev/null
+++ b/config.json.sample
@@ -0,0 +1,5 @@
+{
+ "user":"username",
+ "pass":"password; or delete this line and api key will be used",
+ "api":"aaaaaaaaaaaaaaaaaaaaaaaaaaa"
+}
diff --git a/neocitiesfs.pl b/neocitiesfs.pl
new file mode 100755
index 0000000..3a4b07f
--- /dev/null
+++ b/neocitiesfs.pl
@@ -0,0 +1,456 @@
+#!/usr/bin/perl -w
+use strict;
+use warnings;
+use 5.010;
+use Mojo::UserAgent;
+use JSON;
+use Data::Dumper;
+use Fuse qw(fuse_get_context);
+use POSIX qw(ENOENT EISDIR EINVAL EEXIST ENOTEMPTY EACCES EFBIG
+ EPERM EBADF ENOSPC EMFILE ENOSYS);
+use Smart::Comments;
+use File::Slurper qw(read_text);
+use Mojo::Date;
+use Carp::Always;
+
+use Getopt::Long;
+my $user;
+my $pass;
+my $api;
+my $mountpoint;
+my $config;
+GetOptions ("user=s" => \$user,
+ "pass=s" => \$pass,
+ "api=s" => \$api,
+ "mountpoint=s" => \$mountpoint,
+ "config=s" => \$config,
+ )
+or die("Error in command line arguments\n");
+
+{
+ my $death_string = '';
+ if (! ($pass or $api) ) {
+ if ($config) {
+ if (-e $config and -r $config) {
+ 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;
+ }
+ if (exists $config_ref->{api}) {
+ $api = $config_ref->{api} if not $api;
+ }
+ }
+ else {
+ $death_string .= "$config doesn't exist or is readable\n";
+ }
+ }
+ else {
+ $death_string .= "./neocitiesfs.pl <--user 'username'> <--pass 'pass' || --api 'api key'> or only --config\n";
+ }
+ }
+ if (! $user) {
+ $death_string .= "no --user\n";
+ }
+ if (! ($pass or $api) ) {
+ $death_string .= "no --pass or --api\n";
+ }
+ if (! $mountpoint) {
+ $death_string .= "no --mountpoint\n";
+ }
+ die $death_string if $death_string;
+}
+
+my %files;
+my $suppress_list_update = 0;
+
+# for neocities, it can only be dir or file
+my $TYPE_DIR = 0040;
+my $TYPE_FILE = 0100;
+
+
+die unless try_auth_info();
+get_listing_from_neocities();
+
+sub try_auth_info {
+ my $ua = Mojo::UserAgent->new;
+ my ($tx, $res);
+ if ($pass) {
+ $tx = $ua->get("https://$user:$pass\@neocities.org/api/info");
+ $res = $tx->res;
+ }
+ else {
+ 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.";
+ }
+ return 1;
+}
+
+sub get_listing_from_neocities {
+ return if $suppress_list_update;
+ my $ua = Mojo::UserAgent->new;
+ $ua = $ua->max_redirects(1); # just in case
+
+ my ($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);
+ return 0;
+ }
+ else {
+ return res_errno($res,0);
+ }
+}
+
+sub update_known_files {
+ my ($known_files) = @_;
+ undef %files;
+
+ for my $e (@{ $known_files->{files} }) {
+ my ($dirs, $filename) = get_path_and_file($e->{path});
+ my $date = Mojo::Date->new($e->{updated_at});
+ my $times = $date->epoch;
+ my $size = exists $e->{size} ? $e->{size} : 4096;
+ my $type;
+ my $mode;
+ if ($e->{is_directory}) {
+ $type = $TYPE_DIR;
+ $mode = 0755;
+ $files{ "/$e->{path}" }{'.'} = {
+ type => $TYPE_DIR,
+ mode => 0755,
+ ctime => time(),
+ size => 4096,
+ };
+ $files{ "/$e->{path}" }{'..'} = {
+ type => $TYPE_DIR,
+ mode => 0755,
+ ctime => time(),
+ size => 4096,
+ };
+ } else {
+ $type = $TYPE_FILE;
+ $mode = 0644;
+ }
+ $files{$dirs}{$filename} = {
+ type => $type,
+ mode => $mode,
+ ctime => $times,
+ size => $size,
+ };
+ }
+ $files{'/'}{'.'} = {
+ type => $TYPE_DIR,
+ mode => 0755,
+ ctime => time(),
+ size => 4096,
+ };
+ $files{'/'}{'..'} = {
+ type => $TYPE_DIR,
+ mode => 0755,
+ ctime => time(),
+ size => 4096,
+ };
+ # ## %files
+}
+
+sub get_path_and_file {
+ my $path = shift;
+ my @paths = split '/', $path;
+ my $dirs = '';
+ for (0..($#paths-1)) {
+ $dirs .= "/$paths[$_]"
+ }
+ if ((substr $dirs, 0, 2) eq '//') {
+ substr $dirs, 0, 2, '/'; # '//something' -> '/something'
+ }
+
+ my $filename = $paths[-1];
+
+ if ( ! $dirs ) {
+ $dirs = '/';
+ }
+ if ( ! $filename ) {
+ $filename = '.';
+ }
+ return $dirs, $filename;
+}
+
+sub e_getattr {
+ my ($dirs, $file) = get_path_and_file(shift);
+ return -ENOENT() unless exists($files{$dirs}{$file});
+ my $size = $files{$dirs}{$file}->{size} if exists $files{$dirs}{$file}->{size};
+ my ($modes) = ($files{$dirs}{$file}->{type}<<9) + $files{$dirs}{$file}->{mode};
+ my ($dev, $ino, $rdev, $blocks, $gid, $uid, $nlink, $blksize) = (0,0,0,1,$<,$<,1,1024);
+ my ($atime, $ctime, $mtime);
+ $atime = $ctime = $mtime = $files{$dirs}{$file}->{ctime};
+
+ return ($dev,$ino,$modes,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks);
+}
+
+sub e_getdir {
+ my ($dirs) = @_;
+ return (keys %{ $files{$dirs} } ),0;
+}
+
+sub e_open {
+ # VFS sanity check; it keeps all the necessary state, not much to do here.
+ my ($dirs, $file) = get_path_and_file(shift);
+
+ my ($flags, $fileinfo) = @_;
+ return -ENOENT() unless exists($files{$dirs}{$file});
+ return -EISDIR() if $files{$dirs}{$file}{type} & 0040;
+
+ my $fh = [ rand() ];
+
+ return (0, $fh);
+}
+
+sub e_read {
+ my ($dirs, $file) = get_path_and_file(shift);
+ 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);
+ }
+ else {
+ return -77; # EBADFD, file descrpitor in bad state
+ }
+
+ 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 }
+
+sub e_write {
+ my ($dirs, $file) = get_path_and_file(shift);
+ my ($buf, $off, $fh) = @_;
+ return -ENOENT() unless exists($files{$dirs}{$file});
+ my $res = write_to_neocities($dirs, $file, $buf);
+ return res_errno($res, length($buf));
+}
+
+sub e_mknod {
+ my ($dirs, $file) = get_path_and_file(shift);
+
+ return -EEXIST if exists $files{$dirs}{$file};
+ my $res = write_to_neocities($dirs, $file, '');
+ return res_errno($res, 0);
+}
+
+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" ]});
+ my $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");
+ my $res = $ua->start($tx)->res;
+ }
+
+ $suppress_list_update = 1;
+ my $errno = res_errno($res, 0);
+ if ($errno == 0) {
+ delete $files{$dirs}{$file};
+ }
+ $suppress_list_update = 0;
+ return $errno;
+}
+
+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;
+# my $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/)
+ my $path = shift;
+ my ($dirs, $file) = get_path_and_file($path);
+ return -EEXIST if exists $files{$dirs}{$file};
+
+ $files{$dirs}{$file} = {
+ type => $TYPE_DIR,
+ mode => 0755,
+ ctime => time(),
+ size => 4096,
+ };
+ $files{$path}{'.'} = {
+ type => $TYPE_DIR,
+ mode => 0755,
+ ctime => time(),
+ size => 4096,
+ };
+ $files{$path}{'..'} = {
+ type => $TYPE_DIR,
+ mode => 0755,
+ ctime => time(),
+ size => 4096,
+ };
+ # ## %files
+ return 0;
+}
+
+# neocities '/api/delete' doesn't care about files under the directory tree
+# decided to keep this 'feature' to reduce calls to /api/
+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;
+ }
+ return res_errno($res, 0);
+}
+
+sub e_rename {
+ my ($old_path, $new_path) = @_;
+ my ($old_dirs, $old_file) = get_path_and_file($old_path);
+ my ($new_dirs, $new_file) = get_path_and_file($new_path);
+
+ return -ENOENT if not exists $files{$old_dirs}{$old_file};
+ 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;
+ }
+ return res_errno($res, 0);
+}
+
+sub res_errno {
+ my ($res, $buf_len) = @_;
+ if ($res->is_success) {
+ my $res = get_listing_from_neocities();
+ return $buf_len;
+ }
+ elsif ($res->code == 400) {
+ my $body = from_json($res->body);
+ my %error_codes = (
+ 'invalid_file_type' => -124, ## -EMEDIUMTYPE -- meant to convey that user can't upload this kind of file
+ 'missing_files' => -ENOENT, # when uploading, should work normally with mv
+ 'too_large' => -EFBIG,
+ 'too_many_files' => -EMFILE, #-ENOSPC,
+ 'directory_exists' => -EEXIST,
+ 'missing_arguments' => -EINVAL,
+ 'bad_path' => -EINVAL,
+ 'bad_new_path' => -EINVAL,
+ 'missing_file' => -EBADF, # 'you must provide files to upload'
+ 'rename_error' => -EINVAL,
+ 'missing_filenames' => -EINVAL,
+ 'bad_filename' => -EINVAL,
+ 'cannot_delete_site_directory' => -EPERM,
+ 'cannot_delete_index' => -EPERM,
+ 'email_not_validated' => -56, # EBADRQC # invaild requuest code ¯\_(ツ)_/¯
+ 'invalid_auth' => -EACCES,
+ 'not_found' => -ENOSYS, # calls to /api/ that doesn't exist (not that user should get this error message)
+ )
+ }
+ else {
+ # some kind of error, maybe related to internet?
+ return -EINVAL;
+ }
+}
+
+# this returns mojo's 'res' thing
+sub write_to_neocities {
+ my ($dirs, $file, $buffer) = @_;
+
+ my $ua = Mojo::UserAgent->new();
+ my $asset = Mojo::Asset::Memory->new->add_chunk($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;
+ }
+ 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) = "";
+#$mountpoint = shift(@ARGV) if @ARGV;
+Fuse::main(
+ mountpoint=>$mountpoint,
+ getattr=>"main::e_getattr",
+ getdir =>"main::e_getdir",
+ open =>"main::e_open",
+ statfs =>"main::e_statfs",
+ read =>"main::e_read",
+ write =>"main::e_write",
+ mknod =>"main::e_mknod",
+ unlink =>"main::e_unlink",
+ mkdir =>"main::e_mkdir",
+ rmdir =>"main::e_rmdir",
+ rename =>"main::e_rename",
+ threaded=>0
+);