2023-10-29 15:39:27 -07:00
|
|
|
#!/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,
|
2023-10-29 18:17:14 -07:00
|
|
|
mountopts => "allow_other",
|
2023-10-29 15:39:27 -07:00
|
|
|
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
|
2023-10-29 18:17:14 -07:00
|
|
|
|
2023-10-29 15:39:27 -07:00
|
|
|
);
|