diff options
| author | 2026-01-06 12:18:00 +0100 | |
|---|---|---|
| committer | 2026-01-06 12:18:00 +0100 | |
| commit | 03d270a56b751bc53b83381d9fef21da8d4cbb91 (patch) | |
| tree | 420ee265ed31995479e8660d33cb60c89472a6b0 /snac.c | |
| parent | Implement configurable EXIF stripping for uploaded media (diff) | |
| download | snac2-03d270a56b751bc53b83381d9fef21da8d4cbb91.tar.gz snac2-03d270a56b751bc53b83381d9fef21da8d4cbb91.tar.xz snac2-03d270a56b751bc53b83381d9fef21da8d4cbb91.zip | |
Implement metadata stripping for uploaded videos
- Extend `strip_media` to support video files using `ffmpeg`.
- Use `ffmpeg -map_metadata -1 -c copy` to strip global metadata.
- Support formats: mp4, m4v, mov, webm, mkv, avi.
- Add `ffmpeg_path` configuration option.
- Implement robust relative path handling (`user/` heuristic) to support jailed environments.
- Enforce strict checks on startup: fail if tools (mogrify/ffmpeg) are missing when enabled.
Diffstat (limited to 'snac.c')
| -rw-r--r-- | snac.c | 112 |
1 files changed, 99 insertions, 13 deletions
| @@ -33,6 +33,8 @@ | |||
| 33 | #include <sys/time.h> | 33 | #include <sys/time.h> |
| 34 | #include <sys/stat.h> | 34 | #include <sys/stat.h> |
| 35 | #include <sys/wait.h> | 35 | #include <sys/wait.h> |
| 36 | #include <limits.h> | ||
| 37 | #include <stdlib.h> | ||
| 36 | 38 | ||
| 37 | xs_str *srv_basedir = NULL; | 39 | xs_str *srv_basedir = NULL; |
| 38 | xs_dict *srv_config = NULL; | 40 | xs_dict *srv_config = NULL; |
| @@ -177,12 +179,25 @@ int strip_media(const char *fn) | |||
| 177 | /* strips EXIF data from a file */ | 179 | /* strips EXIF data from a file */ |
| 178 | { | 180 | { |
| 179 | int ret = 0; | 181 | int ret = 0; |
| 182 | |||
| 180 | const xs_val *v = xs_dict_get(srv_config, "strip_exif"); | 183 | const xs_val *v = xs_dict_get(srv_config, "strip_exif"); |
| 181 | 184 | ||
| 182 | if (xs_type(v) == XSTYPE_TRUE) { | 185 | if (xs_type(v) == XSTYPE_TRUE) { |
| 183 | xs *l_fn = xs_tolower_i(xs_dup(fn)); | 186 | /* Heuristic: find 'user/' in the path to make it relative */ |
| 187 | /* This works for ~/user/..., /var/snac/user/..., etc. */ | ||
| 188 | const char *r_fn = strstr(fn, "user/"); | ||
| 189 | |||
| 190 | if (r_fn == NULL) { | ||
| 191 | /* Fallback: try to strip ~/ if present */ | ||
| 192 | if (strncmp(fn, "~/", 2) == 0) | ||
| 193 | r_fn = fn + 2; | ||
| 194 | else | ||
| 195 | r_fn = fn; | ||
| 196 | } | ||
| 184 | 197 | ||
| 185 | /* check extensions */ | 198 | xs *l_fn = xs_tolower_i(xs_dup(r_fn)); |
| 199 | |||
| 200 | /* check image extensions */ | ||
| 186 | if (xs_endswith(l_fn, ".jpg") || xs_endswith(l_fn, ".jpeg") || | 201 | if (xs_endswith(l_fn, ".jpg") || xs_endswith(l_fn, ".jpeg") || |
| 187 | xs_endswith(l_fn, ".png") || xs_endswith(l_fn, ".webp") || | 202 | xs_endswith(l_fn, ".png") || xs_endswith(l_fn, ".webp") || |
| 188 | xs_endswith(l_fn, ".heic") || xs_endswith(l_fn, ".heif") || | 203 | xs_endswith(l_fn, ".heic") || xs_endswith(l_fn, ".heif") || |
| @@ -193,7 +208,7 @@ int strip_media(const char *fn) | |||
| 193 | if (mp == NULL) | 208 | if (mp == NULL) |
| 194 | mp = "mogrify"; | 209 | mp = "mogrify"; |
| 195 | 210 | ||
| 196 | xs *cmd = xs_fmt("%s -strip \"%s\" 2>/dev/null", mp, fn); | 211 | xs *cmd = xs_fmt("cd \"%s\" && %s -auto-orient -strip \"%s\" 2>/dev/null", srv_basedir, mp, r_fn); |
| 197 | 212 | ||
| 198 | ret = system(cmd); | 213 | ret = system(cmd); |
| 199 | 214 | ||
| @@ -203,12 +218,64 @@ int strip_media(const char *fn) | |||
| 203 | code = WEXITSTATUS(ret); | 218 | code = WEXITSTATUS(ret); |
| 204 | 219 | ||
| 205 | if (code == 127) | 220 | if (code == 127) |
| 206 | srv_log(xs_fmt("strip_media: error stripping %s. '%s' not found (exit 127). Set 'mogrify_path' in server.json.", fn, mp)); | 221 | srv_log(xs_fmt("strip_media: error stripping %s. '%s' not found (exit 127). Set 'mogrify_path' in server.json.", r_fn, mp)); |
| 207 | else | 222 | else |
| 208 | srv_log(xs_fmt("strip_media: error stripping %s %d", fn, ret)); | 223 | srv_log(xs_fmt("strip_media: error stripping %s %d", r_fn, ret)); |
| 209 | } | 224 | } |
| 210 | else | 225 | else |
| 211 | srv_debug(1, xs_fmt("strip_media: stripped %s", fn)); | 226 | srv_debug(1, xs_fmt("strip_media: stripped %s", r_fn)); |
| 227 | } | ||
| 228 | else | ||
| 229 | /* check video extensions */ | ||
| 230 | if (xs_endswith(l_fn, ".mp4") || xs_endswith(l_fn, ".m4v") || | ||
| 231 | xs_endswith(l_fn, ".mov") || xs_endswith(l_fn, ".webm") || | ||
| 232 | xs_endswith(l_fn, ".mkv") || xs_endswith(l_fn, ".avi")) { | ||
| 233 | |||
| 234 | const char *fp = xs_dict_get(srv_config, "ffmpeg_path"); | ||
| 235 | if (fp == NULL) | ||
| 236 | fp = "ffmpeg"; | ||
| 237 | |||
| 238 | /* ffmpeg cannot modify in-place, so we need a temp file */ | ||
| 239 | /* we must preserve valid extension for ffmpeg to guess the format */ | ||
| 240 | const char *ext = strrchr(r_fn, '.'); | ||
| 241 | if (ext == NULL) ext = ""; | ||
| 242 | xs *tmp_fn = xs_fmt("%s.tmp%s", r_fn, ext); | ||
| 243 | |||
| 244 | /* -map_metadata -1 strips all global metadata */ | ||
| 245 | /* -c copy copies input streams without re-encoding */ | ||
| 246 | /* we don't silence stderr so we can debug issues */ | ||
| 247 | /* we explicitly cd to srv_basedir to ensure relative paths work */ | ||
| 248 | xs *cmd = xs_fmt("cd \"%s\" && %s -y -i \"%s\" -map_metadata -1 -c copy \"%s\"", srv_basedir, fp, r_fn, tmp_fn); | ||
| 249 | |||
| 250 | ret = system(cmd); | ||
| 251 | |||
| 252 | if (ret != 0) { | ||
| 253 | int code = 0; | ||
| 254 | if (WIFEXITED(ret)) | ||
| 255 | code = WEXITSTATUS(ret); | ||
| 256 | |||
| 257 | if (code == 127) | ||
| 258 | srv_log(xs_fmt("strip_media: error stripping %s. '%s' not found (exit 127). Set 'ffmpeg_path' in server.json.", r_fn, fp)); | ||
| 259 | else { | ||
| 260 | srv_log(xs_fmt("strip_media: error stripping %s %d", r_fn, ret)); | ||
| 261 | srv_log(xs_fmt("strip_media: command was: %s", cmd)); | ||
| 262 | } | ||
| 263 | |||
| 264 | /* try to cleanup, just in case */ | ||
| 265 | /* unlink needs full path too if we are not in basedir */ | ||
| 266 | xs *full_tmp_fn = xs_fmt("%s/%s", srv_basedir, tmp_fn); | ||
| 267 | unlink(full_tmp_fn); | ||
| 268 | } | ||
| 269 | else { | ||
| 270 | /* rename tmp file to original */ | ||
| 271 | /* use full path for source because it was created relative to basedir */ | ||
| 272 | xs *full_tmp_fn = xs_fmt("%s/%s", srv_basedir, tmp_fn); | ||
| 273 | |||
| 274 | if (rename(full_tmp_fn, fn) == 0) | ||
| 275 | srv_debug(1, xs_fmt("strip_media: stripped %s", fn)); | ||
| 276 | else | ||
| 277 | srv_log(xs_fmt("strip_media: error renaming %s to %s", full_tmp_fn, fn)); | ||
| 278 | } | ||
| 212 | } | 279 | } |
| 213 | } | 280 | } |
| 214 | 281 | ||
| @@ -222,14 +289,33 @@ int check_strip_tool(void) | |||
| 222 | int ret = 1; | 289 | int ret = 1; |
| 223 | 290 | ||
| 224 | if (xs_type(v) == XSTYPE_TRUE) { | 291 | if (xs_type(v) == XSTYPE_TRUE) { |
| 225 | const char *mp = xs_dict_get(srv_config, "mogrify_path"); | 292 | /* check mogrify */ |
| 226 | if (mp == NULL) | 293 | { |
| 227 | mp = "mogrify"; | 294 | const char *mp = xs_dict_get(srv_config, "mogrify_path"); |
| 295 | if (mp == NULL) | ||
| 296 | mp = "mogrify"; | ||
| 228 | 297 | ||
| 229 | xs *cmd = xs_fmt("%s -version 2>/dev/null >/dev/null", mp); | 298 | xs *cmd = xs_fmt("%s -version 2>/dev/null >/dev/null", mp); |
| 230 | 299 | ||
| 231 | if (system(cmd) != 0) | 300 | if (system(cmd) != 0) { |
| 232 | ret = 0; | 301 | srv_log(xs_fmt("check_strip_tool: '%s' not working", mp)); |
| 302 | ret = 0; | ||
| 303 | } | ||
| 304 | } | ||
| 305 | |||
| 306 | /* check ffmpeg */ | ||
| 307 | if (ret) { | ||
| 308 | const char *fp = xs_dict_get(srv_config, "ffmpeg_path"); | ||
| 309 | if (fp == NULL) | ||
| 310 | fp = "ffmpeg"; | ||
| 311 | |||
| 312 | xs *cmd = xs_fmt("%s -version 2>/dev/null >/dev/null", fp); | ||
| 313 | |||
| 314 | if (system(cmd) != 0) { | ||
| 315 | srv_log(xs_fmt("check_strip_tool: '%s' not working", fp)); | ||
| 316 | ret = 0; | ||
| 317 | } | ||
| 318 | } | ||
| 233 | } | 319 | } |
| 234 | 320 | ||
| 235 | return ret; | 321 | return ret; |