salvage: add recycle filters and versioning
All checks were successful
Source release / source-package (push) Successful in 53s
All checks were successful
Source release / source-package (push) Successful in 53s
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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
|
||||
# =========================================================================
|
||||
|
||||
553
src/nwsalvage.c
553
src/nwsalvage.c
@@ -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, ×));
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
184
tests/salvage/salvage_ncp_history_smoke.sh
Executable file
184
tests/salvage/salvage_ncp_history_smoke.sh
Executable 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 ]
|
||||
Reference in New Issue
Block a user