salvage: add recycle filters and versioning
All checks were successful
Source release / source-package (push) Successful in 53s

This commit is contained in:
test
2026-05-31 13:53:32 +00:00
committed by Mario Fetka
parent afa4a00f39
commit 5d574078ff
6 changed files with 777 additions and 46 deletions

View File

@@ -11,6 +11,10 @@ typedef int (*nwsalvage_ini_getter)(int entry, char *str,
#define NWSALVAGE_ENABLE_INI_SECTION 51
#define NWSALVAGE_REPOSITORY_INI_SECTION 52
#define NWSALVAGE_FLAGS_INI_SECTION 53
#define NWSALVAGE_SIZE_LIMIT_INI_SECTION 55
#define NWSALVAGE_EXCLUDE_INI_SECTION 56
#define NWSALVAGE_EXCLUDE_DIR_INI_SECTION 57
#define NWSALVAGE_NOVERSIONS_INI_SECTION 58
#define NWSALVAGE_DEFAULT_ENABLED 1
#define NWSALVAGE_DEFAULT_RECYCLE_NAME ".recycle"
@@ -29,12 +33,22 @@ typedef int (*nwsalvage_ini_getter)(int entry, char *str,
#define NWSALVAGE_FINDER_INFO_HEX_LEN 64
#define NWSALVAGE_AFP_ENTRY_ID_MAX 32
#define NWSALVAGE_TRUSTEE_MAX 100
#define NWSALVAGE_PATTERN_MAX 32
#define NWSALVAGE_PATTERN_LEN_MAX 128
struct nwsalvage_config {
int enabled;
char recycle_repository[NWSALVAGE_REPOSITORY_NAME_MAX];
char metadata_repository[NWSALVAGE_REPOSITORY_NAME_MAX];
unsigned int flags;
unsigned long long min_size;
unsigned long long max_size;
unsigned int exclude_count;
char exclude_patterns[NWSALVAGE_PATTERN_MAX][NWSALVAGE_PATTERN_LEN_MAX];
unsigned int exclude_dir_count;
char exclude_dir_patterns[NWSALVAGE_PATTERN_MAX][NWSALVAGE_PATTERN_LEN_MAX];
unsigned int noversions_count;
char noversions_patterns[NWSALVAGE_PATTERN_MAX][NWSALVAGE_PATTERN_LEN_MAX];
};
struct nwsalvage_trustee_entry {
@@ -134,6 +148,14 @@ int nwsalvage_config_parse_repositories(struct nwsalvage_config *config,
const char *line);
int nwsalvage_config_parse_flags(struct nwsalvage_config *config,
const char *line);
int nwsalvage_config_parse_size_limits(struct nwsalvage_config *config,
const char *line);
int nwsalvage_config_parse_exclude(struct nwsalvage_config *config,
const char *line);
int nwsalvage_config_parse_exclude_dir(struct nwsalvage_config *config,
const char *line);
int nwsalvage_config_parse_noversions(struct nwsalvage_config *config,
const char *line);
int nwsalvage_config_load_from_ini(struct nwsalvage_config *config,
nwsalvage_ini_getter getter,
void *data);

View File

@@ -999,13 +999,17 @@
# 54 = recycle directory modes, reserved for Samba-compatible directory_mode
# and subdir_mode handling
#
# 55 = recycle size limits, reserved for Samba-compatible minsize/maxsize
# 55 = recycle size limits: minsize maxsize. Values accept bytes or
# case-insensitive k/kb, m/mb, g/gb suffixes. 0 means unlimited.
#
# 56 = recycle exclude patterns, reserved for Samba-compatible exclude
# 56 = recycle exclude patterns. Matching files are deleted normally and
# are not moved to recycle/salvage. Use '-' for no patterns.
#
# 57 = recycle exclude directories, reserved for Samba-compatible exclude_dir
# 57 = recycle exclude directories. Files below matching directories are
# deleted normally. Use '-' for no patterns.
#
# 58 = recycle no-version patterns, reserved for Samba-compatible noversions
# 58 = recycle no-version patterns. Matching files are still recycled, but
# older recycled copies are replaced instead of using Copy #x names.
#
# 59 = reserved for future salvage cleanup/history policy
#
@@ -1023,6 +1027,10 @@
51 1
52 .recycle .salvage
53 kv
55 0 0
56 -
57 -
58 -
# <<< SMARTHOOK SECTION 51-59 ACTIVE END
# <<< SMARTHOOK SECTION 51-59 END
# =========================================================================

View File

@@ -12,15 +12,22 @@
#include <ctype.h>
#include <errno.h>
#include <fnmatch.h>
#include <limits.h>
#include <stdint.h>
#include <yyjson.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include <utime.h>
#ifndef FNM_CASEFOLD
#define FNM_CASEFOLD 0
#endif
int nwsalvage_repository_name_valid(const char *name)
{
@@ -232,6 +239,168 @@ int nwsalvage_config_parse_flags(struct nwsalvage_config *config,
}
static int nwsalvage_parse_size_value(const char *token,
unsigned long long *out)
{
char *end;
unsigned long long value;
unsigned long long multiplier = 1;
if (!token || !*token || !out) {
errno = EINVAL;
return(-1);
}
errno = 0;
value = strtoull(token, &end, 10);
if (end == token || errno == ERANGE)
return(-1);
if (*end) {
char unit[8];
size_t len = strlen(end);
size_t i;
if (len >= sizeof(unit)) {
errno = EINVAL;
return(-1);
}
for (i = 0; i <= len; i++)
unit[i] = (char)tolower((unsigned char)end[i]);
if (!strcmp(unit, "b") || !strcmp(unit, "byte") || !strcmp(unit, "bytes"))
multiplier = 1;
else if (!strcmp(unit, "k") || !strcmp(unit, "kb"))
multiplier = 1024ULL;
else if (!strcmp(unit, "m") || !strcmp(unit, "mb"))
multiplier = 1024ULL * 1024ULL;
else if (!strcmp(unit, "g") || !strcmp(unit, "gb"))
multiplier = 1024ULL * 1024ULL * 1024ULL;
else {
errno = EINVAL;
return(-1);
}
}
if (value && multiplier > ULLONG_MAX / value) {
errno = ERANGE;
return(-1);
}
*out = value * multiplier;
return(0);
}
int nwsalvage_config_parse_size_limits(struct nwsalvage_config *config,
const char *line)
{
const char *p = line;
char min_token[64];
char max_token[64];
unsigned long long min_size;
unsigned long long max_size;
if (!config || !line) {
errno = EINVAL;
return(-1);
}
if (parse_token(&p, min_token, sizeof(min_token)) < 0 ||
parse_token(&p, max_token, sizeof(max_token)) < 0)
return(-1);
while (*p && isspace((unsigned char)*p)) p++;
if (*p) {
errno = EINVAL;
return(-1);
}
if (nwsalvage_parse_size_value(min_token, &min_size) < 0 ||
nwsalvage_parse_size_value(max_token, &max_size) < 0)
return(-1);
if (max_size && min_size > max_size) {
errno = EINVAL;
return(-1);
}
config->min_size = min_size;
config->max_size = max_size;
return(0);
}
static int nwsalvage_config_parse_patterns(char patterns[][NWSALVAGE_PATTERN_LEN_MAX],
unsigned int *count,
const char *line)
{
const char *p = line;
char token[NWSALVAGE_PATTERN_LEN_MAX];
unsigned int used = 0;
if (!patterns || !count || !line) {
errno = EINVAL;
return(-1);
}
while (*p && isspace((unsigned char)*p)) p++;
if (!*p || *p == '-') {
if (*p == '-') {
p++;
while (*p && isspace((unsigned char)*p)) p++;
if (*p) {
errno = EINVAL;
return(-1);
}
}
*count = 0;
return(0);
}
while (*p) {
if (used >= NWSALVAGE_PATTERN_MAX) {
errno = E2BIG;
return(-1);
}
if (parse_token(&p, token, sizeof(token)) < 0)
return(-1);
strcpy(patterns[used++], token);
while (*p && isspace((unsigned char)*p)) p++;
}
*count = used;
return(0);
}
int nwsalvage_config_parse_exclude(struct nwsalvage_config *config,
const char *line)
{
if (!config) {
errno = EINVAL;
return(-1);
}
return(nwsalvage_config_parse_patterns(config->exclude_patterns,
&config->exclude_count, line));
}
int nwsalvage_config_parse_exclude_dir(struct nwsalvage_config *config,
const char *line)
{
if (!config) {
errno = EINVAL;
return(-1);
}
return(nwsalvage_config_parse_patterns(config->exclude_dir_patterns,
&config->exclude_dir_count, line));
}
int nwsalvage_config_parse_noversions(struct nwsalvage_config *config,
const char *line)
{
if (!config) {
errno = EINVAL;
return(-1);
}
return(nwsalvage_config_parse_patterns(config->noversions_patterns,
&config->noversions_count, line));
}
int nwsalvage_config_load_from_ini(struct nwsalvage_config *config,
nwsalvage_ini_getter getter,
void *data)
@@ -264,6 +433,30 @@ int nwsalvage_config_load_from_ini(struct nwsalvage_config *config,
return(-1);
}
if (getter(NWSALVAGE_SIZE_LIMIT_INI_SECTION,
line, sizeof(line), data)) {
if (nwsalvage_config_parse_size_limits(config, line) < 0)
return(-1);
}
if (getter(NWSALVAGE_EXCLUDE_INI_SECTION,
line, sizeof(line), data)) {
if (nwsalvage_config_parse_exclude(config, line) < 0)
return(-1);
}
if (getter(NWSALVAGE_EXCLUDE_DIR_INI_SECTION,
line, sizeof(line), data)) {
if (nwsalvage_config_parse_exclude_dir(config, line) < 0)
return(-1);
}
if (getter(NWSALVAGE_NOVERSIONS_INI_SECTION,
line, sizeof(line), data)) {
if (nwsalvage_config_parse_noversions(config, line) < 0)
return(-1);
}
return(0);
}
@@ -513,8 +706,7 @@ static const char *metadata_finder_info_hex(
const struct nwsalvage_deleted_entry *entry)
{
return(entry->finder_info_hex && *entry->finder_info_hex ?
entry->finder_info_hex :
"0000000000000000000000000000000000000000000000000000000000000000");
entry->finder_info_hex : NULL);
}
static int validate_hex_string(const char *value, size_t expected_len)
@@ -775,7 +967,8 @@ int nwsalvage_write_metadata(const char *metadata_path,
}
finder_info_hex = metadata_finder_info_hex(entry);
if (validate_hex_string(finder_info_hex, NWSALVAGE_FINDER_INFO_HEX_LEN) < 0)
if (finder_info_hex &&
validate_hex_string(finder_info_hex, NWSALVAGE_FINDER_INFO_HEX_LEN) < 0)
return(-1);
if (!metadata_time_valid(entry->deleted_at) ||
!metadata_time_valid(entry->atime) ||
@@ -838,7 +1031,8 @@ int nwsalvage_write_metadata(const char *metadata_path,
if (!failed && json_add_long(doc, object, "ctime", entry->ctime) < 0)
failed = 1;
if (!failed && json_add_string(doc, object, "finder_info_hex", finder_info_hex) < 0)
if (!failed && finder_info_hex &&
json_add_string(doc, object, "finder_info_hex", finder_info_hex) < 0)
failed = 1;
if (!failed && json_add_string(doc, object, "afp_entry_id",
entry->afp_entry_id ? entry->afp_entry_id : "") < 0)
@@ -990,13 +1184,18 @@ int nwsalvage_read_metadata(const char *metadata_path,
if (!failed && json_get_long(object, "ctime", &entry->ctime) < 0)
failed = 1;
if (!failed && json_get_string_copy(object, "finder_info_hex",
entry->finder_info_hex,
sizeof(entry->finder_info_hex)) < 0)
failed = 1;
if (!failed && validate_hex_string(entry->finder_info_hex,
NWSALVAGE_FINDER_INFO_HEX_LEN) < 0)
failed = 1;
{
yyjson_val *finder = yyjson_obj_get(object, "finder_info_hex");
if (finder) {
if (!yyjson_is_str(finder) ||
strmaxcpy((uint8 *)entry->finder_info_hex,
yyjson_get_str(finder),
sizeof(entry->finder_info_hex) - 1) < 0 ||
validate_hex_string(entry->finder_info_hex,
NWSALVAGE_FINDER_INFO_HEX_LEN) < 0)
failed = 1;
}
}
if (!failed && json_get_string_copy(object, "afp_entry_id",
entry->afp_entry_id,
sizeof(entry->afp_entry_id)) < 0)
@@ -1065,9 +1264,14 @@ int nwsalvage_read_metadata(const char *metadata_path,
return(0);
}
static int nwsalvage_touch_recycled(const char *path,
const struct nwsalvage_config *config,
const struct nwsalvage_deleted_entry *entry);
static int nwsalvage_move_deleted_file(const char *live_path,
const char *recycle_path,
const char *metadata_path,
const struct nwsalvage_config *config,
const struct nwsalvage_deleted_entry *entry)
{
int saved_errno;
@@ -1075,7 +1279,7 @@ static int nwsalvage_move_deleted_file(const char *live_path,
if (!live_path || !*live_path ||
!recycle_path || !*recycle_path ||
!metadata_path || !*metadata_path ||
!entry) {
!config || !entry) {
errno = EINVAL;
return(-1);
}
@@ -1106,6 +1310,13 @@ static int nwsalvage_move_deleted_file(const char *live_path,
if (rename(live_path, recycle_path) < 0)
return(-1);
if (nwsalvage_touch_recycled(recycle_path, config, entry) < 0) {
saved_errno = errno;
(void)rename(recycle_path, live_path);
errno = saved_errno;
return(-1);
}
if (nwsalvage_write_metadata(metadata_path, entry) == 0)
return(0);
@@ -1211,6 +1422,269 @@ static const char *nwsalvage_basename(const char *path)
return(p ? p + 1 : path);
}
static int nwsalvage_dirname_copy(const char *path, char *out, size_t out_len)
{
const char *slash;
size_t len;
if (!path || !out || !out_len) {
errno = EINVAL;
return(-1);
}
slash = strrchr(path, '/');
if (!slash) {
out[0] = '\0';
return(0);
}
len = (size_t)(slash - path);
if (len >= out_len) {
errno = ENAMETOOLONG;
return(-1);
}
memcpy(out, path, len);
out[len] = '\0';
return(0);
}
static int nwsalvage_build_salvage_relative(char *out, size_t out_len,
const struct nwsalvage_config *config,
const char *deleted_by,
const char *relative_path)
{
const char *payload_path;
if (!out || !out_len || !config || !deleted_by || !*deleted_by ||
!relative_path || !*relative_path) {
errno = EINVAL;
return(-1);
}
payload_path = (config->flags & NWSALVAGE_FLAG_KEEP_TREE) ?
relative_path : nwsalvage_basename(relative_path);
if (slprintf(out, (int)out_len - 1, "%s/%s", deleted_by, payload_path) < 0) {
errno = ENAMETOOLONG;
return(-1);
}
return(0);
}
static int nwsalvage_build_copy_relative(char *out, size_t out_len,
const char *base_relative,
unsigned int copy_number)
{
char dir[NWSALVAGE_PATH_MAX];
const char *name;
if (!out || !out_len || !base_relative || !*base_relative || !copy_number) {
errno = EINVAL;
return(-1);
}
name = nwsalvage_basename(base_relative);
if (nwsalvage_dirname_copy(base_relative, dir, sizeof(dir)) < 0)
return(-1);
if (dir[0]) {
if (slprintf(out, (int)out_len - 1, "%s/Copy #%u of %s",
dir, copy_number, name) < 0) {
errno = ENAMETOOLONG;
return(-1);
}
} else {
if (slprintf(out, (int)out_len - 1, "Copy #%u of %s",
copy_number, name) < 0) {
errno = ENAMETOOLONG;
return(-1);
}
}
return(0);
}
static int nwsalvage_path_exists(const char *path)
{
if (access(path, F_OK) == 0)
return(1);
if (errno == ENOENT)
return(0);
return(-1);
}
static int nwsalvage_match_patterns(const char patterns[][NWSALVAGE_PATTERN_LEN_MAX],
unsigned int count,
const char *relative_path)
{
const char *base;
unsigned int i;
if (!relative_path)
return(0);
base = nwsalvage_basename(relative_path);
for (i = 0; i < count; i++) {
const char *pattern = patterns[i];
if (!pattern[0])
continue;
if (fnmatch(pattern, relative_path, FNM_CASEFOLD) == 0 ||
fnmatch(pattern, base, FNM_CASEFOLD) == 0)
return(1);
}
return(0);
}
static int nwsalvage_match_exclude_dir(const struct nwsalvage_config *config,
const char *relative_path)
{
char dir[NWSALVAGE_PATH_MAX];
unsigned int i;
if (!config || !relative_path)
return(0);
if (nwsalvage_dirname_copy(relative_path, dir, sizeof(dir)) < 0)
return(0);
if (!dir[0])
return(0);
for (i = 0; i < config->exclude_dir_count; i++) {
const char *pattern = config->exclude_dir_patterns[i];
const char *p = pattern;
if (!*p)
continue;
while (*p == '/')
p++;
if (fnmatch(p, dir, FNM_CASEFOLD) == 0 ||
!strcasecmp(p, dir) ||
(strlen(dir) > strlen(p) &&
!strncasecmp(dir, p, strlen(p)) && dir[strlen(p)] == '/'))
return(1);
}
return(0);
}
static int nwsalvage_should_skip(const struct nwsalvage_config *config,
const char *relative_path,
const struct stat *stb)
{
unsigned long long size;
if (!config || !relative_path || !stb)
return(0);
size = (unsigned long long)stb->st_size;
if (config->min_size && size < config->min_size)
return(1);
if (config->max_size && size > config->max_size)
return(1);
if (nwsalvage_match_patterns(config->exclude_patterns,
config->exclude_count, relative_path))
return(1);
if (nwsalvage_match_exclude_dir(config, relative_path))
return(1);
return(0);
}
static int nwsalvage_select_paths(const struct nwsalvage_config *config,
const char *volume_root,
const char *base_relative,
char *selected_relative,
size_t selected_relative_len,
char *recycle_path,
size_t recycle_path_len,
char *metadata_path,
size_t metadata_path_len,
char *recycle_relative_path,
size_t recycle_relative_path_len,
char *metadata_relative_path,
size_t metadata_relative_path_len)
{
unsigned int copy;
int recycle_exists;
int metadata_exists;
int use_versions;
if (!config || !volume_root || !base_relative || !selected_relative ||
!recycle_path || !metadata_path || !recycle_relative_path ||
!metadata_relative_path) {
errno = EINVAL;
return(-1);
}
use_versions = (config->flags & NWSALVAGE_FLAG_VERSIONS) &&
!nwsalvage_match_patterns(config->noversions_patterns,
config->noversions_count, base_relative);
for (copy = 0; copy < 1000000; copy++) {
if (!copy) {
if (strlen(base_relative) >= selected_relative_len) {
errno = ENAMETOOLONG;
return(-1);
}
strcpy(selected_relative, base_relative);
} else {
if (!use_versions)
break;
if (nwsalvage_build_copy_relative(selected_relative,
selected_relative_len,
base_relative, copy) < 0)
return(-1);
}
if (nwsalvage_build_recycle_path(recycle_path, recycle_path_len,
config, volume_root,
selected_relative) < 0 ||
nwsalvage_build_metadata_path(metadata_path, metadata_path_len,
config, volume_root,
selected_relative) < 0 ||
nwsalvage_build_recycle_relative_path(recycle_relative_path,
recycle_relative_path_len,
config, selected_relative) < 0 ||
nwsalvage_build_metadata_relative_path(metadata_relative_path,
metadata_relative_path_len,
config, selected_relative) < 0)
return(-1);
recycle_exists = nwsalvage_path_exists(recycle_path);
if (recycle_exists < 0)
return(-1);
metadata_exists = nwsalvage_path_exists(metadata_path);
if (metadata_exists < 0)
return(-1);
if (!recycle_exists && !metadata_exists)
return(0);
if (!use_versions) {
if (recycle_exists && unlink(recycle_path) < 0)
return(-1);
if (metadata_exists && unlink(metadata_path) < 0)
return(-1);
return(0);
}
}
errno = EEXIST;
return(-1);
}
static int nwsalvage_touch_recycled(const char *path,
const struct nwsalvage_config *config,
const struct nwsalvage_deleted_entry *entry)
{
struct utimbuf times;
time_t now;
if (!path || !config || !entry)
return(0);
if (!(config->flags & (NWSALVAGE_FLAG_TOUCH | NWSALVAGE_FLAG_TOUCH_MTIME)))
return(0);
now = time(NULL);
times.actime = now;
times.modtime = (config->flags & NWSALVAGE_FLAG_TOUCH_MTIME) ?
now : (time_t)entry->mtime;
return(utime(path, &times));
}
static void nwsalvage_format_deleted_by(char *out, size_t out_len)
{
if (!out || !out_len)
@@ -1273,11 +1747,11 @@ static void nwsalvage_fill_afp_metadata(const char *unixname,
uint32 resource_size = 0;
memset(finder_info, 0, sizeof(finder_info));
if (nwatalk_get_finder_info(unixname, finder_info, sizeof(finder_info)) != 0)
memset(finder_info, 0, sizeof(finder_info));
nwsalvage_format_hex(finder_info_hex, finder_info_hex_len,
finder_info, sizeof(finder_info));
entry->finder_info_hex = finder_info_hex;
if (nwatalk_get_finder_info(unixname, finder_info, sizeof(finder_info)) == 0) {
nwsalvage_format_hex(finder_info_hex, finder_info_hex_len,
finder_info, sizeof(finder_info));
entry->finder_info_hex = finder_info_hex;
}
if (nwatalk_get_entry_id(unixname, &entry_id) == 0)
slprintf(afp_entry_id, (int)afp_entry_id_len - 1, "0x%08x", entry_id);
@@ -1360,7 +1834,8 @@ int nwsalvage_capture_node_delete(int volume, const char *unixname,
char relative_path[NWSALVAGE_PATH_MAX];
char recycle_path[NWSALVAGE_PATH_MAX];
char metadata_path[NWSALVAGE_PATH_MAX];
char salvage_relative_path[NWSALVAGE_PATH_MAX];
char base_salvage_relative_path[NWSALVAGE_PATH_MAX];
char selected_salvage_relative_path[NWSALVAGE_PATH_MAX];
char recycle_relative_path[NWSALVAGE_PATH_MAX];
char metadata_relative_path[NWSALVAGE_PATH_MAX];
char original_path[NWSALVAGE_PATH_MAX];
@@ -1393,27 +1868,24 @@ int nwsalvage_capture_node_delete(int volume, const char *unixname,
config.metadata_repository))
return(1);
nwsalvage_format_deleted_by(deleted_by, sizeof(deleted_by));
if (slprintf(salvage_relative_path, sizeof(salvage_relative_path) - 1,
"%s/%s", deleted_by, relative_path) < 0) {
errno = ENAMETOOLONG;
return(-1);
}
if (nwsalvage_should_skip(&config, relative_path, stb))
return(1);
if (nwsalvage_build_recycle_path(recycle_path, sizeof(recycle_path),
&config, volume_root,
salvage_relative_path) < 0 ||
nwsalvage_build_metadata_path(metadata_path, sizeof(metadata_path),
&config, volume_root,
salvage_relative_path) < 0 ||
nwsalvage_build_recycle_relative_path(recycle_relative_path,
sizeof(recycle_relative_path),
&config,
salvage_relative_path) < 0 ||
nwsalvage_build_metadata_relative_path(metadata_relative_path,
sizeof(metadata_relative_path),
&config,
salvage_relative_path) < 0)
nwsalvage_format_deleted_by(deleted_by, sizeof(deleted_by));
if (nwsalvage_build_salvage_relative(base_salvage_relative_path,
sizeof(base_salvage_relative_path),
&config, deleted_by, relative_path) < 0)
return(-1);
if (nwsalvage_select_paths(&config, volume_root, base_salvage_relative_path,
selected_salvage_relative_path,
sizeof(selected_salvage_relative_path),
recycle_path, sizeof(recycle_path),
metadata_path, sizeof(metadata_path),
recycle_relative_path,
sizeof(recycle_relative_path),
metadata_relative_path,
sizeof(metadata_relative_path)) < 0)
return(-1);
if (nw_get_volume_name(volume, (uint8 *)volume_name,
@@ -1427,8 +1899,7 @@ int nwsalvage_capture_node_delete(int volume, const char *unixname,
}
memset(&entry, 0, sizeof(entry));
memset(finder_info_hex, '0', NWSALVAGE_FINDER_INFO_HEX_LEN);
finder_info_hex[NWSALVAGE_FINDER_INFO_HEX_LEN] = '\0';
finder_info_hex[0] = '\0';
afp_entry_id[0] = '\0';
entry.source = "mars_nwe";
@@ -1455,7 +1926,7 @@ int nwsalvage_capture_node_delete(int volume, const char *unixname,
nwsalvage_fill_trustees(volume, unixname, stb, &entry);
if (nwsalvage_move_deleted_file(unixname, recycle_path,
metadata_path, &entry) < 0)
metadata_path, &config, &entry) < 0)
return(-1);
return(0);

View File

@@ -46,6 +46,26 @@ add_custom_target(salvage_ncp_delete_smoke_suite ALL
DEPENDS ${SALVAGE_NCP_DELETE_SMOKE_SCRIPT}
)
set(SALVAGE_NCP_HISTORY_SMOKE_SCRIPT
${CMAKE_CURRENT_BINARY_DIR}/salvage_ncp_history_smoke.sh
)
add_custom_command(
OUTPUT ${SALVAGE_NCP_HISTORY_SMOKE_SCRIPT}
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_SOURCE_DIR}/salvage_ncp_history_smoke.sh
${SALVAGE_NCP_HISTORY_SMOKE_SCRIPT}
COMMAND ${CMAKE_COMMAND} -E env chmod +x ${SALVAGE_NCP_HISTORY_SMOKE_SCRIPT}
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/salvage_ncp_history_smoke.sh
COMMENT "Copying salvage NCP history smoke helper"
VERBATIM
)
add_custom_target(salvage_ncp_history_smoke_suite ALL
DEPENDS ${SALVAGE_NCP_HISTORY_SMOKE_SCRIPT}
)
find_path(SALVAGE_NCPFS_INCLUDE_DIR
NAMES ncp/nwcalls.h ncp/ncplib.h
)

View File

@@ -21,11 +21,18 @@ Default repository names come from the `nwserv.conf`/`nw.ini` template options:
51 1
52 .recycle .salvage
53 kv
55 0 0
56 -
57 -
58 -
```
Section `53` is a compact behaviour-flag string inspired by Samba
`vfs_recycle` options. Flags can be combined in one token: `k` = keeptree,
`v` = versions, `t` = touch, and `m` = touch_mtime.
`v` = versions, `t` = touch, and `m` = touch_mtime. Section `55`
contains min/max file sizes and accepts raw bytes or case-insensitive `kb`,
`mb`, and `gb` suffixes. Sections `56`, `57`, and `58` mirror Samba's
`exclude`, `exclude_dir`, and `noversions` pattern lists.
Expected layout for a deleted `SYS:PUBLIC/PMDFLTS.INI` owned by `SUPERVISOR`.
The sidecar JSON must carry the server-only metadata needed for exact recover,
@@ -85,3 +92,22 @@ Example:
This check lives under `tests/salvage` rather than `tests/afp` because later
salvage scan/recover/purge and versioning tests should share the same fixture.
AFP can use the same backend later as an adapter for `0x13`.
## NCP history/version smoke
`salvage_ncp_history_smoke.sh` deletes the same NetWare path twice through
NCP and verifies Samba-compatible version naming. With the `v` flag enabled,
the first recycled payload keeps its original name and the second is written as
`Copy #1 of NAME` in the same recycle directory. The `.salvage` sidecar uses
the same selected payload name with a `.json` suffix.
Example:
```sh
./tests/salvage/salvage_ncp_history_smoke.sh \
-S MARS -U SUPERVISOR -P secret \
--path SYS:PUBLIC/SLVGHIST.TXT \
--unix-path /var/mars_nwe/SYS/public/SLVGHIST.TXT \
--volume-root /var/mars_nwe/SYS \
--out /tmp/mars-salvage-history-report.txt
```

View File

@@ -0,0 +1,184 @@
#!/usr/bin/env bash
# Verify Samba-compatible salvage versioning through the normal NCP delete path.
set -u
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
SERVER="MARS"
USER_NAME="SUPERVISOR"
PASSWORD=""
NETWARE_PATH="SYS:PUBLIC/SLVGHIST.TXT"
UNIX_PATH="/var/mars_nwe/SYS/public/SLVGHIST.TXT"
VOLUME_ROOT=""
RECYCLE_REPOSITORY=".recycle"
SALVAGE_REPOSITORY=".salvage"
DELETED_BY=""
OUT_FILE=""
FAILURES=0
usage() {
cat <<USAGE
Usage: $0 -S SERVER -U USER -P PASSWORD [options]
Options:
--path NETWARE_PATH NetWare file path to create/delete twice
--unix-path UNIX_PATH Unix path corresponding to --path
--volume-root PATH Unix root of the NetWare volume
--deleted-by NAME Expected directory below .recycle/.salvage (default: USER)
--recycle-name NAME Recycle repository name (default: $RECYCLE_REPOSITORY)
--salvage-name NAME Salvage metadata repository name (default: $SALVAGE_REPOSITORY)
--out FILE Write the complete report to FILE as well as stdout
-h, --help Show this help
The script expects salvage option 53 to include the 'v' versions flag. It
creates and deletes the same NetWare path twice through ncp_delete_smoke and
then checks for the Samba-compatible second name: Copy #1 of NAME.
USAGE
}
while [ $# -gt 0 ]; do
case "$1" in
-S|--server) SERVER=$2; shift 2 ;;
-U|--user) USER_NAME=$2; shift 2 ;;
-P|--password) PASSWORD=$2; shift 2 ;;
--path) NETWARE_PATH=$2; shift 2 ;;
--unix-path) UNIX_PATH=$2; shift 2 ;;
--volume-root) VOLUME_ROOT=$2; shift 2 ;;
--deleted-by) DELETED_BY=$2; shift 2 ;;
--recycle-name) RECYCLE_REPOSITORY=$2; shift 2 ;;
--salvage-name) SALVAGE_REPOSITORY=$2; shift 2 ;;
--out) OUT_FILE=$2; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown argument: $1" >&2; usage >&2; exit 2 ;;
esac
done
[ -n "$PASSWORD" ] || { echo "Missing password. Use -P PASSWORD." >&2; exit 2; }
[ -n "$DELETED_BY" ] || DELETED_BY=$USER_NAME
case "$UNIX_PATH" in /*) ;; *) echo "--unix-path must be absolute" >&2; exit 2 ;; esac
case "$NETWARE_PATH" in *:*) ;; *) echo "--path must include a volume prefix" >&2; exit 2 ;; esac
REPORT_TMP=$(mktemp "${TMPDIR:-/tmp}/mars-salvage-history.XXXXXX")
trap 'rm -f "$REPORT_TMP"' EXIT INT TERM
emit() { printf '%s\n' "$*" | tee -a "$REPORT_TMP"; }
section() { emit ""; emit "## $*"; }
fail_check() { emit "$*"; FAILURES=$((FAILURES + 1)); }
finish_report() {
section "Summary"
emit "failures=$FAILURES"
if [ -n "$OUT_FILE" ]; then
cp "$REPORT_TMP" "$OUT_FILE"
emit "report=$OUT_FILE"
fi
}
count_path_components() {
local path=$1 old_ifs=$IFS count=0 part
path=${path#*:}; path=${path#/}; path=${path#\\}; path=${path//\\//}
IFS=/
for part in $path; do [ -n "$part" ] && count=$((count + 1)); done
IFS=$old_ifs
printf '%s\n' "$count"
}
compute_unix_volume_root() {
local root components
root=$(dirname -- "$UNIX_PATH")
components=$(count_path_components "$NETWARE_PATH") || return 1
while [ "$components" -gt 1 ]; do root=$(dirname -- "$root"); components=$((components - 1)); done
printf '%s\n' "$root"
}
relative_to_volume_root() {
local path=$1 root=$2
case "$path" in "$root"/*) printf '%s\n' "${path#$root/}" ;; *) return 1 ;; esac
}
run_delete() {
local label=$1 status
section "$label"
emit "\$ ./ncp_delete_smoke -S '$SERVER' -U '$USER_NAME' -P ****** '$NETWARE_PATH'"
"$SCRIPT_DIR/ncp_delete_smoke" -S "$SERVER" -U "$USER_NAME" -P "$PASSWORD" "$NETWARE_PATH" 2>&1 | tee -a "$REPORT_TMP"
status=${PIPESTATUS[0]}
emit "[exit=$status]"
if [ "$status" -ne 0 ]; then
emit "warning: NCP helper exited $status; validating salvage artifacts anyway"
fi
}
check_file() {
local label=$1 path=$2
section "$label"
emit "path=$path"
[ -f "$path" ] || { fail_check "missing file: $path"; return 1; }
return 0
}
cat_metadata() {
local path=$1
section "metadata JSON: $path"
emit "\$ cat '$path'"
cat "$path" 2>&1 | tee -a "$REPORT_TMP"
local status=${PIPESTATUS[0]}
emit "[exit=$status]"
[ "$status" -eq 0 ] || fail_check "could not cat metadata: $path"
}
section "mars_nwe salvage NCP history smoke"
emit "date=$(date -Is)"
emit "server=$SERVER"
emit "user=$USER_NAME"
emit "path=$NETWARE_PATH"
emit "unix_path=$UNIX_PATH"
emit "deleted_by=$DELETED_BY"
if [ -z "$VOLUME_ROOT" ]; then
VOLUME_ROOT=$(compute_unix_volume_root) || { fail_check "could not compute volume root"; VOLUME_ROOT=""; }
fi
RELATIVE_PATH=""
if [ -n "$VOLUME_ROOT" ]; then
RELATIVE_PATH=$(relative_to_volume_root "$UNIX_PATH" "$VOLUME_ROOT") || fail_check "could not derive relative path"
fi
BASENAME=$(basename -- "$RELATIVE_PATH")
DIRNAME=$(dirname -- "$RELATIVE_PATH")
[ "$DIRNAME" = "." ] && DIRNAME=""
COPY_NAME="Copy #1 of $BASENAME"
if [ -n "$DIRNAME" ]; then
FIRST_REL="$DELETED_BY/$DIRNAME/$BASENAME"
SECOND_REL="$DELETED_BY/$DIRNAME/$COPY_NAME"
else
FIRST_REL="$DELETED_BY/$BASENAME"
SECOND_REL="$DELETED_BY/$COPY_NAME"
fi
FIRST_RECYCLE="$VOLUME_ROOT/$RECYCLE_REPOSITORY/$FIRST_REL"
SECOND_RECYCLE="$VOLUME_ROOT/$RECYCLE_REPOSITORY/$SECOND_REL"
FIRST_META="$VOLUME_ROOT/$SALVAGE_REPOSITORY/$FIRST_REL.json"
SECOND_META="$VOLUME_ROOT/$SALVAGE_REPOSITORY/$SECOND_REL.json"
emit "volume_root=$VOLUME_ROOT"
emit "relative_path=$RELATIVE_PATH"
emit "first_recycle=$FIRST_RECYCLE"
emit "second_recycle=$SECOND_RECYCLE"
emit "first_metadata=$FIRST_META"
emit "second_metadata=$SECOND_META"
section "pre-clean test history artifacts"
rm -f -- "$FIRST_META" "$SECOND_META" "$FIRST_RECYCLE" "$SECOND_RECYCLE" 2>&1 | tee -a "$REPORT_TMP"
run_delete "NCP create/delete #1"
run_delete "NCP create/delete #2"
check_file "first recycle payload" "$FIRST_RECYCLE"
check_file "second recycle payload" "$SECOND_RECYCLE"
check_file "first salvage metadata" "$FIRST_META" && cat_metadata "$FIRST_META"
check_file "second salvage metadata" "$SECOND_META" && cat_metadata "$SECOND_META"
if [ -f "$SECOND_META" ]; then
grep -q "Copy #1 of $BASENAME" "$SECOND_META" || fail_check "second metadata missing Copy #1 path"
fi
finish_report
[ "$FAILURES" -eq 0 ]