rsync flags: -a, -n, -p, --exclude, --delete

Feature changes across 15 files:

- -a/--archive: enables -c -m -M (no -s, which slows transfers)
- -n/--dry-run: scan + print without connecting or transferring
- -p <port>: custom SSH port (passed as -p to ssh via execvp)
- --exclude <pattern>: glob-based filename filtering in scanner
- --delete: sender collects file manifest; receiver deletes unlisted files

Protocol: added STATUS_MANIFEST, use_delete field in Config wire format.
New utilities: glob_match(), delete_extras() with recursive directory walk.
Scanner: accepts exclude patterns; skips matching entries.
SSH transport: switched from execlp to execvp for dynamic port arg.
Server + multiprocessing: handle STATUS_MANIFEST in both single and
multithreaded receive paths.

Builds clean, all 7 tests pass.
This commit is contained in:
2026-07-16 12:39:05 +02:00
parent f778d1903c
commit 284bf8f8bc
15 changed files with 281 additions and 20 deletions
+15
View File
@@ -23,11 +23,16 @@ Config *config_create(char *version, char *send_directory,
config->use_compression = use_compression;
config->use_metadata = use_metadata;
config->show_progress = false;
config->dry_run = false;
config->use_delete = false;
config->compression_level = compression_level;
config->use_sendfile = use_sendfile;
config->chunk_size = chunk_size > 0 ? chunk_size : DEFAULT_CHUNK_SIZE;
config->ssh_port = 22;
config->transport = TRANSPORT_TCP;
config->ssh_destination = NULL;
config->exclude_patterns = NULL;
config->exclude_count = 0;
return config;
}
@@ -57,6 +62,9 @@ void config_delete(Config *config) {
free(config->send_directory);
free(config->receive_root_directory);
free(config->ssh_destination);
for (int i = 0; i < config->exclude_count; i++)
free(config->exclude_patterns[i]);
free(config->exclude_patterns);
free(config);
}
@@ -72,6 +80,7 @@ void config_send(int file_descriptor, Config *config) {
send_int(file_descriptor, config->compression_level);
send_int(file_descriptor, (int)config->chunk_size);
send_int(file_descriptor, config->use_sendfile);
send_int(file_descriptor, config->use_delete);
if (receive_status(file_descriptor) != STATUS_OK) {
perror("Error transmitting config!");
exit(EXIT_FAILURE);
@@ -91,8 +100,14 @@ Config *config_receive(int file_descriptor) {
config->compression_level = receive_int(file_descriptor);
config->chunk_size = (unsigned long long)receive_int(file_descriptor);
config->use_sendfile = receive_int(file_descriptor);
config->use_delete = receive_int(file_descriptor);
config->show_progress = false;
config->dry_run = false;
config->ssh_port = 22;
config->transport = TRANSPORT_TCP;
config->ssh_destination = NULL;
config->exclude_patterns = NULL;
config->exclude_count = 0;
send_status(file_descriptor, STATUS_OK);
return config;
}
+5
View File
@@ -19,10 +19,15 @@ typedef struct Config {
bool use_sendfile;
bool use_metadata;
bool show_progress;
bool dry_run;
bool use_delete;
int compression_level;
unsigned long long chunk_size;
int ssh_port;
TransportType transport;
char *ssh_destination;
char **exclude_patterns;
int exclude_count;
} Config;
#define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024)
+17
View File
@@ -1,4 +1,5 @@
#include "multiprocessing.h"
#include "array_list.h"
#include "chunk.h"
#include "compression.h"
#include "config.h"
@@ -36,6 +37,11 @@ PipelineContextSender *pipeline_context_sender_create(Config *config,
}
void pipeline_context_sender_destroy(PipelineContextSender *context) {
if (context->manifest) {
for (int i = 0; i < context->manifest->size; i++)
free(context->manifest->items[i]);
array_list_delete(context->manifest);
}
config_delete(context->config);
queue_destroy(context->queue_scanner);
queue_destroy(context->queue_loader);
@@ -119,6 +125,17 @@ int receive_thread(void *pipeline_context) {
}
status = receive_status(file_descriptor);
}
if (status == STATUS_MANIFEST) {
int count = receive_int(file_descriptor);
ArrayList *manifest = array_list_create(free);
for (int i = 0; i < count; i++)
array_list_add(manifest, receive_str(file_descriptor));
delete_extras(context->config->receive_root_directory, manifest);
for (int i = 0; i < manifest->size; i++)
free(manifest->items[i]);
array_list_delete(manifest);
status = receive_status(file_descriptor);
}
mtx_lock(&context->mutex);
context->receiver_done = true;
cnd_signal(&context->condition_not_empty);
+2
View File
@@ -3,6 +3,7 @@
#include <threads.h>
#include "array_list.h"
#include "config.h"
#include "file.h"
#include "queue.h"
@@ -19,6 +20,7 @@ typedef struct {
cnd_t condition_not_full_loader;
cnd_t condition_not_empty_loader;
bool loader_done;
ArrayList *manifest;
} PipelineContextSender;
typedef struct PipelineContextReceiver {
+1 -1
View File
@@ -5,7 +5,7 @@
#include <stddef.h>
typedef int Status;
enum NET_STATUS { STATUS_OK, STATUS_ERROR, STATUS_FINISHED, STATUS_NEXT, STATUS_CHUNK };
enum NET_STATUS { STATUS_OK, STATUS_ERROR, STATUS_FINISHED, STATUS_NEXT, STATUS_CHUNK, STATUS_MANIFEST };
void io_set_fds(int read_fd, int write_fd);
void send_n_data(int file_descriptor, void *data, size_t data_size);
+21 -5
View File
@@ -42,7 +42,7 @@ static int parse_remote_dest(const char *dest, RemoteDest *r) {
return 0;
}
Client *client_connect_ssh(char *destination) {
Client *client_connect_ssh(char *destination, int port) {
RemoteDest r;
if (parse_remote_dest(destination, &r) != 0) {
fprintf(stderr, "Invalid remote destination: %s\n", destination);
@@ -90,10 +90,26 @@ Client *client_connect_ssh(char *destination) {
else
snprintf(ssh_user, sizeof(ssh_user), "%s", r.host);
execlp("ssh", "ssh", "-o", "Compression=no", "-o",
"ControlMaster=auto", "-o",
"ControlPath=~/.cache/fastsync-%r@%h:%p", ssh_user,
"fastsync-server", "--stdio", (char *)NULL);
char *ssh_argv[16];
int ac = 0;
char port_str[16];
ssh_argv[ac++] = "ssh";
ssh_argv[ac++] = "-o";
ssh_argv[ac++] = "Compression=no";
ssh_argv[ac++] = "-o";
ssh_argv[ac++] = "ControlMaster=auto";
ssh_argv[ac++] = "-o";
ssh_argv[ac++] = "ControlPath=~/.cache/fastsync-%r@%h:%p";
if (port > 0 && port != 22) {
ssh_argv[ac++] = "-p";
snprintf(port_str, sizeof(port_str), "%d", port);
ssh_argv[ac++] = port_str;
}
ssh_argv[ac++] = ssh_user;
ssh_argv[ac++] = "fastsync-server";
ssh_argv[ac++] = "--stdio";
ssh_argv[ac] = NULL;
execvp("ssh", ssh_argv);
perror("exec of ssh failed");
ssize_t wret = write(exec_pipe[1], "x", 1);
(void)wret;
+1 -1
View File
@@ -3,6 +3,6 @@
#include "transport_tcp.h"
Client *client_connect_ssh(char *destination);
Client *client_connect_ssh(char *destination, int port);
#endif
+72
View File
@@ -1,9 +1,12 @@
#include "utils.h"
#include "array_list.h"
#include "libgen.h"
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
void mkdir_r(char *path) {
char *path_duplicate = malloc(strlen(path) + 1);
@@ -44,6 +47,75 @@ char *str_dup(const char *string) {
return new_string;
}
bool glob_match(const char *pattern, const char *str) {
while (*pattern) {
if (*pattern == '*') {
pattern++;
while (*str && *str != '/') {
if (glob_match(pattern, str))
return true;
str++;
}
return glob_match(pattern, str);
} else if (*pattern == '?') {
if (!*str || *str == '/')
return false;
pattern++;
str++;
} else {
if (*pattern != *str)
return false;
pattern++;
str++;
}
}
return *str == '\0';
}
static void delete_extras_walk(const char *abs_path, const char *rel_path,
ArrayList *manifest) {
DIR *dir = opendir(abs_path);
if (!dir)
return;
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
continue;
char *child_abs = path_cat((char *)abs_path, entry->d_name);
char *child_rel = path_cat((char *)rel_path, entry->d_name);
struct stat st;
if (stat(child_abs, &st) != 0) {
free(child_abs);
free(child_rel);
continue;
}
if (S_ISDIR(st.st_mode)) {
delete_extras_walk(child_abs, child_rel, manifest);
} else {
// Check if relative path is in manifest
bool found = false;
for (int i = 0; i < manifest->size; i++) {
if (strcmp((char *)manifest->items[i], child_rel) == 0) {
found = true;
break;
}
}
if (!found) {
unlink(child_abs);
fprintf(stderr, " Deleted: %s\n", child_rel);
}
}
free(child_abs);
free(child_rel);
}
closedir(dir);
rmdir(abs_path);
}
void delete_extras(const char *dest_root, ArrayList *manifest) {
delete_extras_walk(dest_root, "", manifest);
}
char *path_cat(char *path1, char *path2) {
if (path1 == NULL || *path1 == '\0')
return str_dup(path2);
+5
View File
@@ -1,8 +1,13 @@
#ifndef UTILS_H
#define UTILS_H
#include "array_list.h"
#include <stdbool.h>
void mkdir_r(char *path);
char *str_dup(const char *string);
char *path_cat(char *path1, char *path2);
bool glob_match(const char *pattern, const char *str);
void delete_extras(const char *dest_root, ArrayList *manifest);
#endif