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