2024-12-14 10:09:47 -06:00
|
|
|
#include "plugin.h"
|
|
|
|
|
#include "filer.h"
|
2024-12-13 10:51:10 -06:00
|
|
|
#include <iostream>
|
|
|
|
|
#include <fstream>
|
|
|
|
|
#include <string>
|
|
|
|
|
#include <filesystem>
|
|
|
|
|
#include <sys/stat.h>
|
|
|
|
|
#include <time.h>
|
|
|
|
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
2024-12-14 10:09:47 -06:00
|
|
|
class LocalFiler : public IPlugin, public Filer {
|
2024-12-13 10:51:10 -06:00
|
|
|
public:
|
|
|
|
|
LocalFiler() {}
|
|
|
|
|
|
|
|
|
|
file_data setRoot(std::string _root) {
|
|
|
|
|
struct file_data fd;
|
|
|
|
|
try {
|
|
|
|
|
// Always convert to absolute path
|
|
|
|
|
fs::path new_root = fs::absolute(fs::weakly_canonical(_root));
|
|
|
|
|
fd.path = new_root.string().c_str();
|
|
|
|
|
|
|
|
|
|
// Create directory if it doesn't exist
|
|
|
|
|
if (!fs::exists(new_root)) {
|
|
|
|
|
if (!fs::create_directories(new_root)) {
|
|
|
|
|
fd.error = {FilerStatusCodes::NoPermission, "Failed to create root directory"};
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set the absolute root path
|
|
|
|
|
root = new_root;
|
|
|
|
|
// Reset CWD to root-relative path
|
|
|
|
|
cwd = "/";
|
|
|
|
|
|
|
|
|
|
} catch (const fs::filesystem_error& ex) {
|
|
|
|
|
logger->print(LOGLEVEL_ERROR, "setRoot error: %s", ex.what());
|
|
|
|
|
fd.error = {FilerStatusCodes::Exception, ex.what()};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
file_data setCWD(std::string _cwd) {
|
|
|
|
|
struct file_data fd;
|
|
|
|
|
try {
|
|
|
|
|
// Always convert to absolute path
|
|
|
|
|
fs::path new_cwd = fs::weakly_canonical(_cwd);
|
|
|
|
|
fd.path = new_cwd.string().c_str();
|
|
|
|
|
|
|
|
|
|
// Create directory if it doesn't exist
|
|
|
|
|
if (!fs::exists(root / new_cwd)) {
|
|
|
|
|
if (!fs::create_directories(root / new_cwd)) {
|
|
|
|
|
fd.error = {FilerStatusCodes::NoPermission, "Failed to create cwd directory"};
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cwd = new_cwd;
|
|
|
|
|
} catch (const fs::filesystem_error& ex) {
|
|
|
|
|
logger->print(LOGLEVEL_ERROR, "setCWD error: %s", ex.what());
|
|
|
|
|
fd.error = {FilerStatusCodes::Exception, ex.what()};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fs::path getRoot() {
|
|
|
|
|
return this->root;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fs::path getCWD() {
|
|
|
|
|
return this->cwd;
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-31 22:15:53 -06:00
|
|
|
bool resolvePath(const std::string& path, fs::path* target) {
|
2024-12-13 10:51:10 -06:00
|
|
|
try {
|
|
|
|
|
if (path.empty() || path == ".") {
|
2024-12-31 22:15:53 -06:00
|
|
|
*target = root / cwd.relative_path();
|
|
|
|
|
return true;
|
2024-12-13 10:51:10 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (path[0] == '/') {
|
|
|
|
|
// Absolute path relative to FTP root
|
2024-12-31 22:15:53 -06:00
|
|
|
*target = root / path.substr(1);
|
2024-12-13 10:51:10 -06:00
|
|
|
} else {
|
|
|
|
|
// Relative to current directory
|
2024-12-31 22:15:53 -06:00
|
|
|
*target = root / cwd.relative_path() / path;
|
2024-12-13 10:51:10 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove .. and . components
|
2024-12-31 22:15:53 -06:00
|
|
|
*target = fs::weakly_canonical(*target);
|
2024-12-13 10:51:10 -06:00
|
|
|
|
|
|
|
|
// Make sure we haven't escaped the root
|
2024-12-31 22:15:53 -06:00
|
|
|
if (!(*target).string().starts_with(root.string()))
|
|
|
|
|
return false;
|
2024-12-13 10:51:10 -06:00
|
|
|
|
2024-12-31 22:15:53 -06:00
|
|
|
return true;
|
2024-12-13 10:51:10 -06:00
|
|
|
} catch (const std::exception& e) {
|
|
|
|
|
logger->print(LOGLEVEL_ERROR, "Path resolution error: %s", e.what());
|
2024-12-31 22:15:53 -06:00
|
|
|
return false;
|
2024-12-13 10:51:10 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
file_data traverse(std::string dir) {
|
|
|
|
|
struct file_data fd;
|
|
|
|
|
try {
|
2024-12-31 22:15:53 -06:00
|
|
|
fs::path requested_path;
|
|
|
|
|
if (!resolvePath(dir, &requested_path)) {
|
|
|
|
|
fd.error = file_error{FilerStatusCodes::NoPermission, "Invalid Permissions"};
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-13 10:51:10 -06:00
|
|
|
if (dir.empty() || dir == ".") {
|
2024-12-31 22:15:53 -06:00
|
|
|
fd.path = requested_path.c_str();
|
2024-12-13 10:51:10 -06:00
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify the requested path exists and is within root
|
|
|
|
|
if (!fs::exists(requested_path)) {
|
|
|
|
|
fd.error = file_error{FilerStatusCodes::NotFound, "Directory Not Found"};
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!fs::is_directory(requested_path)) {
|
|
|
|
|
fd.error = file_error{FilerStatusCodes::NotFound, "Not a Directory"};
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Make sure path is within root directory
|
|
|
|
|
fs::path rel_path = fs::relative(requested_path, root);
|
|
|
|
|
if (rel_path.string().find("..") == 0) {
|
|
|
|
|
fd.error = file_error{FilerStatusCodes::NoPermission, "Invalid Permissions"};
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update current working directory relative to root
|
|
|
|
|
cwd = "/" + rel_path.string();
|
|
|
|
|
fd.path = requested_path.string().c_str();
|
|
|
|
|
|
|
|
|
|
} catch (const fs::filesystem_error& ex) {
|
|
|
|
|
logger->print(LOGLEVEL_ERROR, "Path fallback: %s", ex.what());
|
|
|
|
|
fd.error = file_error{FilerStatusCodes::Exception, ex.what()};
|
|
|
|
|
}
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
file_data createDirectory(std::string dir) {
|
|
|
|
|
struct file_data fd;
|
|
|
|
|
try {
|
2024-12-31 22:15:53 -06:00
|
|
|
fs::path resolved;
|
|
|
|
|
if (!resolvePath(dir, &resolved)) {
|
|
|
|
|
fd.error = file_error{FilerStatusCodes::NoPermission, "Invalid Permissions"};
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-13 10:51:10 -06:00
|
|
|
fd.path = resolved.string().c_str();
|
|
|
|
|
|
|
|
|
|
if (fs::exists(resolved)) {
|
|
|
|
|
fd.error = file_error{FilerStatusCodes::FileExists, "Directory Already Exists"};
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fs::create_directory(resolved);
|
|
|
|
|
} catch (const std::filesystem::filesystem_error& ex) {
|
|
|
|
|
logger->print(LOGLEVEL_ERROR, ex.what());
|
|
|
|
|
fd.error = file_error{FilerStatusCodes::Exception, ex.what()};
|
|
|
|
|
}
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
file_data fileSize(std::string name) {
|
|
|
|
|
struct file_data fd;
|
|
|
|
|
try {
|
2024-12-31 22:15:53 -06:00
|
|
|
fs::path resolved;
|
|
|
|
|
if (!resolvePath(name, &resolved)) {
|
|
|
|
|
fd.error = file_error{FilerStatusCodes::NoPermission, "Invalid Permissions"};
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
2024-12-13 10:51:10 -06:00
|
|
|
fd.path = resolved.string().c_str();
|
|
|
|
|
|
|
|
|
|
if (!fs::exists(resolved)) {
|
|
|
|
|
fd.error = file_error{FilerStatusCodes::NotFound, "File Not Found"};
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (type == 'A') {
|
|
|
|
|
fd.error = file_error{FilerStatusCodes::InvalidTransferMode, "Refusing to transfer in ASCII mode"};
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::ifstream infile(resolved, std::ios::in|std::ios::binary|std::ios::ate);
|
|
|
|
|
if (infile.is_open()) {
|
|
|
|
|
fd.size = infile.tellg();
|
|
|
|
|
} else {
|
|
|
|
|
fd.error = file_error{FilerStatusCodes::NoPermission, "Unable to open file"};
|
|
|
|
|
}
|
|
|
|
|
} catch (const std::filesystem::filesystem_error& ex) {
|
|
|
|
|
logger->print(LOGLEVEL_ERROR, ex.what());
|
|
|
|
|
fd.error = file_error{FilerStatusCodes::Exception, ex.what()};
|
|
|
|
|
}
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
file_data deleteFile(std::string name) {
|
|
|
|
|
struct file_data fd;
|
|
|
|
|
try {
|
2024-12-31 22:15:53 -06:00
|
|
|
fs::path resolved;
|
|
|
|
|
if (!resolvePath(name, &resolved)) {
|
|
|
|
|
fd.error = file_error{FilerStatusCodes::NoPermission, "Invalid Permissions"};
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
2024-12-13 10:51:10 -06:00
|
|
|
fd.path = resolved.string().c_str();
|
|
|
|
|
|
|
|
|
|
if (!fs::exists(resolved)) {
|
|
|
|
|
fd.error = file_error{FilerStatusCodes::NotFound, "File Not Found"};
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (fs::is_directory(resolved) && !fs::is_empty(resolved)) {
|
|
|
|
|
fd.error = file_error{FilerStatusCodes::DirectoryNotEmpty, "Directory not empty"};
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-31 15:41:06 -06:00
|
|
|
if (fs::remove_all(resolved) == 0) {
|
2024-12-13 10:51:10 -06:00
|
|
|
fd.error = file_error{FilerStatusCodes::NoPermission, "Unable to delete file"};
|
|
|
|
|
}
|
|
|
|
|
} catch (const std::filesystem::filesystem_error& ex) {
|
|
|
|
|
logger->print(LOGLEVEL_ERROR, ex.what());
|
|
|
|
|
fd.error = file_error{FilerStatusCodes::Exception, ex.what()};
|
|
|
|
|
}
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
file_data readFile(std::string name) {
|
|
|
|
|
struct file_data fd;
|
|
|
|
|
try {
|
2024-12-31 22:15:53 -06:00
|
|
|
fs::path resolved;
|
|
|
|
|
if (!resolvePath(name, &resolved)) {
|
|
|
|
|
fd.error = file_error{FilerStatusCodes::NoPermission, "Invalid Permissions"};
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
2024-12-13 10:51:10 -06:00
|
|
|
fd.path = resolved.string().c_str();
|
|
|
|
|
|
|
|
|
|
if (!fs::exists(resolved)) {
|
|
|
|
|
fd.error = file_error{FilerStatusCodes::NotFound, "File Not Found"};
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (type != 'A') {
|
|
|
|
|
// Create a shared_ptr to manage the ifstream
|
|
|
|
|
fd.stream = std::make_shared<std::ifstream>(resolved, std::ios::in|std::ios::binary);
|
|
|
|
|
|
|
|
|
|
if (fd.stream && fd.stream->is_open()) {
|
|
|
|
|
// Get file size
|
|
|
|
|
fd.stream->seekg(0, std::ios::end);
|
|
|
|
|
fd.size = fd.stream->tellg();
|
|
|
|
|
fd.stream->seekg(0, std::ios::beg);
|
|
|
|
|
} else {
|
|
|
|
|
fd.error = file_error{FilerStatusCodes::NoPermission, "Unable to open file"};
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
fd.error = file_error{FilerStatusCodes::InvalidTransferMode, "Refusing to transfer in ASCII mode"};
|
|
|
|
|
}
|
|
|
|
|
} catch (const std::filesystem::filesystem_error& ex) {
|
|
|
|
|
logger->print(LOGLEVEL_ERROR, ex.what());
|
|
|
|
|
fd.error = file_error{FilerStatusCodes::Exception, ex.what()};
|
|
|
|
|
}
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
file_data writeFile(std::string name, unsigned char* data, int size, bool append = false) {
|
|
|
|
|
struct file_data fd;
|
|
|
|
|
try {
|
2024-12-31 22:15:53 -06:00
|
|
|
fs::path resolved;
|
|
|
|
|
if (!resolvePath(name, &resolved)) {
|
|
|
|
|
fd.error = file_error{FilerStatusCodes::NoPermission, "Invalid Permissions"};
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
2024-12-13 10:51:10 -06:00
|
|
|
fd.path = resolved.string().c_str();
|
|
|
|
|
|
|
|
|
|
std::ios_base::openmode omode = std::ios::out|std::ios::binary;
|
|
|
|
|
if (append) omode |= std::ios::app;
|
|
|
|
|
|
|
|
|
|
std::ofstream outfile(resolved, omode);
|
|
|
|
|
if (outfile.is_open()) {
|
|
|
|
|
outfile.write((char *)data, size);
|
|
|
|
|
outfile.close();
|
|
|
|
|
fd.size = size;
|
|
|
|
|
} else {
|
|
|
|
|
fd.error = file_error{FilerStatusCodes::NoPermission, "Unable to open file"};
|
|
|
|
|
}
|
|
|
|
|
} catch (const std::filesystem::filesystem_error& ex) {
|
|
|
|
|
logger->print(LOGLEVEL_ERROR, ex.what());
|
|
|
|
|
fd.error = file_error{FilerStatusCodes::Exception, ex.what()};
|
|
|
|
|
}
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct file_data renameFile(const std::string& from, const std::string& to) {
|
|
|
|
|
struct file_data fd;
|
2024-12-31 22:15:53 -06:00
|
|
|
fs::path src_path;
|
|
|
|
|
if (!resolvePath(from, &src_path)) {
|
|
|
|
|
fd.error = file_error{FilerStatusCodes::NoPermission, "Invalid Source Permissions"};
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
fs::path dst_path;
|
|
|
|
|
if (!resolvePath(to, &dst_path)) {
|
|
|
|
|
fd.error = file_error{FilerStatusCodes::NoPermission, "Invalid Destination Permissions"};
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
2024-12-13 10:51:10 -06:00
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Check if destination already exists
|
|
|
|
|
if (std::filesystem::exists(dst_path)) {
|
|
|
|
|
fd.error = {FilerStatusCodes::FileExists, "Destination file already exists"};
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Perform the rename operation
|
|
|
|
|
std::filesystem::rename(src_path, dst_path);
|
|
|
|
|
fd.error = {0, "OK"};
|
|
|
|
|
|
|
|
|
|
} catch (const std::filesystem::filesystem_error& e) {
|
|
|
|
|
fd.error = {FilerStatusCodes::AccessDenied, e.what()};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
file_data list(std::string path = ".") {
|
|
|
|
|
struct file_data fd;
|
|
|
|
|
try {
|
2024-12-31 22:15:53 -06:00
|
|
|
fs::path resolved;
|
|
|
|
|
if (!resolvePath(path, &resolved)) {
|
|
|
|
|
fd.error = file_error{FilerStatusCodes::NoPermission, "Invalid Permissions"};
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
2024-12-13 10:51:10 -06:00
|
|
|
fd.path = resolved.string().c_str();
|
|
|
|
|
|
|
|
|
|
if (!fs::exists(resolved)) {
|
|
|
|
|
fd.error = file_error{FilerStatusCodes::NotFound, "File Not Found"};
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::ostringstream listStream;
|
|
|
|
|
for(const auto& p : fs::directory_iterator(resolved,
|
|
|
|
|
fs::directory_options::skip_permission_denied)) {
|
|
|
|
|
|
|
|
|
|
struct stat fstat;
|
|
|
|
|
struct tm *time;
|
|
|
|
|
time_t rawtime;
|
|
|
|
|
char timebuff[80];
|
|
|
|
|
|
2024-12-31 15:40:11 -06:00
|
|
|
if (lstat(p.path().c_str(), &fstat) == -1) {
|
2024-12-13 10:51:10 -06:00
|
|
|
fd.error = file_error{FilerStatusCodes::NoPermission, "Unable to stat file"};
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Convert time_t to tm struct */
|
|
|
|
|
rawtime = fstat.st_mtime;
|
|
|
|
|
time = localtime(&rawtime);
|
|
|
|
|
strftime(timebuff, 80, "%b %d %H:%M", time);
|
|
|
|
|
|
|
|
|
|
// God should've smitten me before I wrote such attrocities.
|
|
|
|
|
char* line;
|
2024-12-31 15:40:11 -06:00
|
|
|
fs::perms fperms = fs::symlink_status(p).permissions();
|
|
|
|
|
bool is_link = S_ISLNK(fstat.st_mode);
|
2024-12-13 10:51:10 -06:00
|
|
|
asprintf(
|
|
|
|
|
&line,
|
2024-12-31 15:40:11 -06:00
|
|
|
"%c%c%c%c%c%c%c%c%c%c %4u %4u %4u %12u %s %s%s\r\n",
|
|
|
|
|
is_link?'l':(p.is_directory()?'d':'-'),
|
2024-12-13 10:51:10 -06:00
|
|
|
(fperms & fs::perms::owner_read) != fs::perms::none?'r':'-',
|
|
|
|
|
(fperms & fs::perms::owner_write) != fs::perms::none?'w':'-',
|
|
|
|
|
(fperms & fs::perms::owner_exec) != fs::perms::none?'x':'-',
|
|
|
|
|
(fperms & fs::perms::group_read) != fs::perms::none?'r':'-',
|
|
|
|
|
(fperms & fs::perms::group_write) != fs::perms::none?'w':'-',
|
|
|
|
|
(fperms & fs::perms::group_exec) != fs::perms::none?'x':'-',
|
|
|
|
|
(fperms & fs::perms::others_read) != fs::perms::none?'r':'-',
|
|
|
|
|
(fperms & fs::perms::others_write) != fs::perms::none?'w':'-',
|
|
|
|
|
(fperms & fs::perms::others_exec) != fs::perms::none?'x':'-',
|
2024-12-31 15:40:11 -06:00
|
|
|
fstat.st_nlink,
|
2024-12-13 10:51:10 -06:00
|
|
|
fstat.st_uid,
|
|
|
|
|
fstat.st_gid,
|
|
|
|
|
fstat.st_size,
|
|
|
|
|
timebuff,
|
2024-12-31 15:40:11 -06:00
|
|
|
p.path().filename().c_str(),
|
|
|
|
|
is_link?(" -> " + fs::read_symlink(p).string()).c_str():""
|
2024-12-13 10:51:10 -06:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
listStream << std::string(line);
|
|
|
|
|
free(line);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fd.size = listStream.tellp();
|
|
|
|
|
fd.bin = new char[fd.size];
|
|
|
|
|
std::strncpy(fd.bin, listStream.str().c_str(), fd.size);
|
|
|
|
|
|
|
|
|
|
} catch (const std::filesystem::filesystem_error& ex) {
|
|
|
|
|
logger->print(LOGLEVEL_ERROR, ex.what());
|
|
|
|
|
fd.error = file_error{FilerStatusCodes::Exception, ex.what()};
|
|
|
|
|
}
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
private:
|
|
|
|
|
fs::path root;
|
|
|
|
|
fs::path cwd;
|
|
|
|
|
char type = 'I';
|
|
|
|
|
};
|
|
|
|
|
|
2024-12-14 10:09:47 -06:00
|
|
|
IMPLEMENT_PLUGIN(LocalFiler, Filer, "local", "Local filesystem implementation", "1.0.0")
|