commit 2ad60da1ede63846978d1276790f9dc3b60d3961
Author: Kris Yotam <krisyotam@protonmail.com>
Date: Mon, 16 Feb 2026 02:40:52 -0600
Initial commit: suploader - Simple Uploader
Suckless C tool that uploads URLs to web archive services
(Internet Archive, archive.ph, Wikiwix). Reads URLs from
a file or stdin with configurable rate limiting, random
selection, and daemon mode.
Diffstat:
| A | .claude/CLAUDE.md | | | 141 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | .gitignore | | | 2 | ++ |
| A | LICENSE | | | 21 | +++++++++++++++++++++ |
| A | Makefile | | | 45 | +++++++++++++++++++++++++++++++++++++++++++++ |
| A | config.h | | | 29 | +++++++++++++++++++++++++++++ |
| A | services.c | | | 285 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | services.h | | | 25 | +++++++++++++++++++++++++ |
| A | suploader.c | | | 339 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | util.c | | | 90 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | util.h | | | 21 | +++++++++++++++++++++ |
10 files changed, 998 insertions(+), 0 deletions(-)
diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md
@@ -0,0 +1,141 @@
+# suploader — CLAUDE.md
+
+## Project
+
+suploader (Simple Uploader) is a suckless tool that uploads URLs to web
+archive services. It reads a file of URLs (one per line) or stdin and
+submits each to the Internet Archive (Wayback Machine), archive.today
+(archive.ph), and Wikiwix. Supports configurable rate limiting, random
+URL selection, and daemon mode for continuous background archiving.
+
+## Coding Standards — Suckless C Style
+
+All code in this project MUST follow the suckless.org coding style:
+
+### Language
+- C99 (ISO/IEC 9899:1999), no extensions
+- POSIX.1-2008 (`_POSIX_C_SOURCE 200809L`)
+
+### Indentation & Whitespace
+- Tabs for indentation (1 tab = 1 level)
+- Spaces for alignment only, never for indentation
+- No tabs except at the beginning of a line
+- Maximum line length: 79 characters
+
+### Comments
+- Use `/* */` only, never `//`
+- Comment fallthrough cases in switch statements
+
+### Variables
+- All declarations at the top of the block
+- Pointer `*` adjacent to variable name: `char *p`, not `char* p`
+- No C99 `bool`; use `int` (0/1)
+- Global/static variables not used outside TU must be `static`
+
+### Functions
+- Return type on its own line
+- Function name at column 0 on next line (enables `grep ^funcname`)
+- Opening `{` on its own line for functions
+- Functions not used outside their file: `static`
+
+```c
+static void
+usage(void)
+{
+ fprintf(stderr, "usage: suploader [-v] [-d secs] file\n");
+ exit(1);
+}
+```
+
+### Braces
+- Opening `{` on same line for control flow (if, for, while, switch)
+- Closing `}` on its own line unless continuing (else, do-while)
+- Use braces even for single statements when sibling branches use them
+
+### Naming
+- lowercase_with_underscores for functions and variables
+- UPPERCASE for macros and constants
+- CamelCase for typedef'd struct types
+- No `_t` suffix (reserved by POSIX)
+- Prefix module functions with module name
+
+### Control Flow
+- Space after `if`, `for`, `while`, `switch`
+- No space after `(` or before `)`
+- Use `goto` for cleanup/unwind, not nested ifs
+- Return/exit early on failure
+- Test against 0, not -1: `if (func() < 0)`
+
+### Error Handling
+- All allocation checked; goto cleanup on failure
+- `die()` for fatal errors (prints message, exits)
+- `warn()` for recoverable errors (prints, continues)
+
+### File Organization Order
+1. License header
+2. System includes (alphabetical)
+3. Local includes
+4. Macros
+5. Type definitions
+6. Function declarations
+7. Global variables
+8. Function definitions (same order as declarations)
+
+### Headers
+- System headers first, alphabetical
+- Local headers after blank line
+- No cyclic dependencies
+- Include only what is needed
+
+## Architecture
+
+### Module Layout
+
+| Module | Prefix | File | Responsibility |
+|--------|--------|------|----------------|
+| Main | — | suploader.c | Entry point, URL file I/O, daemon loop |
+| Services | `svc_` | services.c | Archive service submission (IA, archive.ph, Wikiwix) |
+| Utilities | `die`, `warn`, `x*` | util.c | Memory wrappers, string ops, error handling |
+| Config | — | config.h | Compile-time constants (timeouts, delays, user agent) |
+
+### Architecture Rules
+- **Separate compilation.** Every .c file compiles independently.
+- **No dynamic loading.** All features compiled in.
+- **libcurl only.** Single external dependency for HTTP.
+- **Fire-and-forget.** Submit and move on; no verification polling.
+- **Text file interface.** One URL per line, Unix-philosophy I/O.
+
+## Build
+
+```sh
+make # build suploader binary
+make clean # remove build artifacts
+make install # install to /usr/local/bin
+```
+
+Dependencies: `libcurl` (via pkg-config)
+
+## Usage
+
+```sh
+# Upload URLs from a file
+suploader urls.txt
+
+# Read from stdin
+cat urls.txt | suploader -
+
+# Daemon mode (loop forever, process one URL per cycle)
+suploader -D urls.txt
+
+# Custom delay between submissions (seconds)
+suploader -d 60 urls.txt
+
+# Verbose output
+suploader -v urls.txt
+```
+
+## Git Conventions
+
+- No `Co-Authored-By: Claude` lines
+- Commit messages: imperative, <72 chars, no period
+- One logical change per commit
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,2 @@
+suploader
+*.o
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,21 @@
+MIT/X Consortium License
+
+(c) 2026 Kris Yotam <krisyotam@proton.me>
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff --git a/Makefile b/Makefile
@@ -0,0 +1,45 @@
+# suploader - Simple Uploader
+# See LICENSE file for copyright and license details.
+
+VERSION = 0.1.0
+
+# paths
+PREFIX = /usr/local
+MANPREFIX = $(PREFIX)/share/man
+
+# includes and libs
+INCS = `pkg-config --cflags libcurl`
+LIBS = `pkg-config --libs libcurl`
+
+# flags
+CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_POSIX_C_SOURCE=200809L -DVERSION=\"$(VERSION)\"
+CFLAGS = -std=c99 -pedantic -Wall -Wextra -Os $(INCS) $(CPPFLAGS)
+LDFLAGS = $(LIBS)
+
+# compiler
+CC = cc
+
+# sources
+SRC = suploader.c services.c util.c
+OBJ = $(SRC:.c=.o)
+
+all: suploader
+
+.c.o:
+ $(CC) $(CFLAGS) -c $<
+
+suploader: $(OBJ)
+ $(CC) -o $@ $(OBJ) $(LDFLAGS)
+
+clean:
+ rm -f suploader $(OBJ)
+
+install: all
+ mkdir -p $(DESTDIR)$(PREFIX)/bin
+ cp -f suploader $(DESTDIR)$(PREFIX)/bin
+ chmod 755 $(DESTDIR)$(PREFIX)/bin/suploader
+
+uninstall:
+ rm -f $(DESTDIR)$(PREFIX)/bin/suploader
+
+.PHONY: all clean install uninstall
diff --git a/config.h b/config.h
@@ -0,0 +1,29 @@
+/* See LICENSE file for copyright and license details.
+ * suploader - Simple Uploader
+ * configuration header
+ */
+
+#ifndef CONFIG_H
+#define CONFIG_H
+
+/* Program metadata */
+#define PROG_NAME "suploader"
+#define PROG_VERSION "0.1.0"
+
+/* Network settings */
+#define USER_AGENT "suploader/0.1 (+https://krisyotam.com)"
+#define CONNECT_TIMEOUT 30L
+#define REQUEST_TIMEOUT 60L
+#define MAX_REDIRECTS 10L
+
+/* Rate limiting */
+#define DEFAULT_DELAY 48 /* seconds between submissions */
+
+/* Retry settings */
+#define MAX_RETRIES 3
+#define RETRY_BASE 2 /* base seconds for exponential backoff */
+
+/* URL limits */
+#define MAX_URL_LEN 4096
+
+#endif /* CONFIG_H */
diff --git a/services.c b/services.c
@@ -0,0 +1,285 @@
+/* See LICENSE file for copyright and license details. */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <curl/curl.h>
+
+#include "config.h"
+#include "services.h"
+#include "util.h"
+
+static CURL *curl_handle = NULL;
+
+/* Discard response body */
+static size_t
+discard_cb(void *contents, size_t size, size_t nmemb, void *userp)
+{
+ (void)contents;
+ (void)userp;
+ return size * nmemb;
+}
+
+static int
+is_transient(long code)
+{
+ return code == 429 || code == 500 || code == 502 ||
+ code == 503 || code == 504;
+}
+
+/* Perform a GET request, return HTTP status or -1 on error */
+static long
+http_get(const char *url)
+{
+ CURLcode res;
+ long status;
+ int attempt;
+
+ for (attempt = 0; attempt < MAX_RETRIES; attempt++) {
+ if (attempt > 0) {
+ unsigned int delay;
+
+ delay = RETRY_BASE * (1 << (attempt - 1));
+ sleep(delay);
+ }
+
+ curl_easy_reset(curl_handle);
+ curl_easy_setopt(curl_handle, CURLOPT_URL, url);
+ curl_easy_setopt(curl_handle,
+ CURLOPT_WRITEFUNCTION, discard_cb);
+ curl_easy_setopt(curl_handle,
+ CURLOPT_USERAGENT, USER_AGENT);
+ curl_easy_setopt(curl_handle,
+ CURLOPT_FOLLOWLOCATION, 1L);
+ curl_easy_setopt(curl_handle,
+ CURLOPT_MAXREDIRS, MAX_REDIRECTS);
+ curl_easy_setopt(curl_handle,
+ CURLOPT_CONNECTTIMEOUT, CONNECT_TIMEOUT);
+ curl_easy_setopt(curl_handle,
+ CURLOPT_TIMEOUT, REQUEST_TIMEOUT);
+ curl_easy_setopt(curl_handle,
+ CURLOPT_SSL_VERIFYPEER, 1L);
+ curl_easy_setopt(curl_handle,
+ CURLOPT_SSL_VERIFYHOST, 2L);
+
+ res = curl_easy_perform(curl_handle);
+ if (res != CURLE_OK)
+ continue;
+
+ curl_easy_getinfo(curl_handle,
+ CURLINFO_RESPONSE_CODE, &status);
+
+ if (is_transient(status))
+ continue;
+
+ return status;
+ }
+ return -1;
+}
+
+/* Perform a POST request, return HTTP status or -1 on error */
+static long
+http_post(const char *url, const char *postdata)
+{
+ CURLcode res;
+ long status;
+ int attempt;
+
+ for (attempt = 0; attempt < MAX_RETRIES; attempt++) {
+ if (attempt > 0) {
+ unsigned int delay;
+
+ delay = RETRY_BASE * (1 << (attempt - 1));
+ sleep(delay);
+ }
+
+ curl_easy_reset(curl_handle);
+ curl_easy_setopt(curl_handle, CURLOPT_URL, url);
+ curl_easy_setopt(curl_handle,
+ CURLOPT_POSTFIELDS, postdata);
+ curl_easy_setopt(curl_handle,
+ CURLOPT_WRITEFUNCTION, discard_cb);
+ curl_easy_setopt(curl_handle,
+ CURLOPT_USERAGENT, USER_AGENT);
+ curl_easy_setopt(curl_handle,
+ CURLOPT_FOLLOWLOCATION, 1L);
+ curl_easy_setopt(curl_handle,
+ CURLOPT_MAXREDIRS, MAX_REDIRECTS);
+ curl_easy_setopt(curl_handle,
+ CURLOPT_CONNECTTIMEOUT, CONNECT_TIMEOUT);
+ curl_easy_setopt(curl_handle,
+ CURLOPT_TIMEOUT, REQUEST_TIMEOUT);
+ curl_easy_setopt(curl_handle,
+ CURLOPT_SSL_VERIFYPEER, 1L);
+ curl_easy_setopt(curl_handle,
+ CURLOPT_SSL_VERIFYHOST, 2L);
+
+ res = curl_easy_perform(curl_handle);
+ if (res != CURLE_OK)
+ continue;
+
+ curl_easy_getinfo(curl_handle,
+ CURLINFO_RESPONSE_CODE, &status);
+
+ if (is_transient(status))
+ continue;
+
+ return status;
+ }
+ return -1;
+}
+
+void
+svc_init(void)
+{
+ curl_global_init(CURL_GLOBAL_ALL);
+ curl_handle = curl_easy_init();
+ if (!curl_handle)
+ die("curl_easy_init failed");
+}
+
+void
+svc_cleanup(void)
+{
+ if (curl_handle) {
+ curl_easy_cleanup(curl_handle);
+ curl_handle = NULL;
+ }
+ curl_global_cleanup();
+}
+
+int
+svc_submit_ia(const char *url, int verbose)
+{
+ char *save_url;
+ size_t len;
+ long status;
+
+ len = strlen("https://web.archive.org/save/") +
+ strlen(url) + 1;
+ save_url = xmalloc(len);
+ snprintf(save_url, len,
+ "https://web.archive.org/save/%s", url);
+
+ if (verbose)
+ fprintf(stderr, " IA: %s\n", save_url);
+
+ status = http_get(save_url);
+ free(save_url);
+
+ if (status >= 200 && status < 400) {
+ if (verbose)
+ fprintf(stderr, " IA: OK (%ld)\n", status);
+ return 0;
+ }
+
+ warn("IA: HTTP %ld for %s", status, url);
+ return -1;
+}
+
+int
+svc_submit_archiveph(const char *url, int verbose)
+{
+ char *postdata, *escaped;
+ size_t len;
+ long status;
+
+ escaped = curl_easy_escape(curl_handle, url, 0);
+ if (!escaped) {
+ warn("archive.ph: URL escape failed for %s", url);
+ return -1;
+ }
+
+ len = strlen("url=") + strlen(escaped) +
+ strlen("&submit=") + 1;
+ postdata = xmalloc(len);
+ snprintf(postdata, len, "url=%s&submit=", escaped);
+ curl_free(escaped);
+
+ if (verbose)
+ fprintf(stderr, " archive.ph: submitting %s\n",
+ url);
+
+ status = http_post("https://archive.ph/submit/", postdata);
+ free(postdata);
+
+ if (status >= 200 && status < 400) {
+ if (verbose)
+ fprintf(stderr,
+ " archive.ph: OK (%ld)\n", status);
+ return 0;
+ }
+
+ /* archive.ph returns 3xx redirect on success */
+ if (status >= 300 && status < 400) {
+ if (verbose)
+ fprintf(stderr,
+ " archive.ph: OK (redirect)\n");
+ return 0;
+ }
+
+ warn("archive.ph: HTTP %ld for %s", status, url);
+ return -1;
+}
+
+int
+svc_submit_wikiwix(const char *url, int verbose)
+{
+ char *cache_url;
+ size_t len;
+ long status;
+
+ len = strlen("https://archive.wikiwix.com/cache/?url=") +
+ strlen(url) + 1;
+ cache_url = xmalloc(len);
+ snprintf(cache_url, len,
+ "https://archive.wikiwix.com/cache/?url=%s", url);
+
+ if (verbose)
+ fprintf(stderr, " Wikiwix: %s\n", cache_url);
+
+ status = http_get(cache_url);
+ free(cache_url);
+
+ if (status >= 200 && status < 400) {
+ if (verbose)
+ fprintf(stderr,
+ " Wikiwix: OK (%ld)\n", status);
+ return 0;
+ }
+
+ warn("Wikiwix: HTTP %ld for %s", status, url);
+ return -1;
+}
+
+int
+svc_submit(const char *url, int services, int verbose)
+{
+ int ok;
+
+ ok = 0;
+
+ /* Skip .onion URLs - archive services can't reach them */
+ if (strstr(url, ".onion/") || strstr(url, ".onion:")) {
+ if (verbose)
+ fprintf(stderr,
+ " skip: .onion URL\n");
+ return 0;
+ }
+
+ if (services & SVC_IA) {
+ if (svc_submit_ia(url, verbose) == 0)
+ ok++;
+ }
+ if (services & SVC_WIKIWIX) {
+ if (svc_submit_wikiwix(url, verbose) == 0)
+ ok++;
+ }
+ if (services & SVC_ARCHIVEPH) {
+ if (svc_submit_archiveph(url, verbose) == 0)
+ ok++;
+ }
+
+ return ok;
+}
diff --git a/services.h b/services.h
@@ -0,0 +1,25 @@
+/* See LICENSE file for copyright and license details. */
+
+#ifndef SERVICES_H
+#define SERVICES_H
+
+/* Archive service identifiers */
+#define SVC_IA (1 << 0) /* Internet Archive (Wayback Machine) */
+#define SVC_ARCHIVEPH (1 << 1) /* archive.today / archive.ph */
+#define SVC_WIKIWIX (1 << 2) /* Wikiwix */
+#define SVC_ALL (SVC_IA | SVC_ARCHIVEPH | SVC_WIKIWIX)
+
+/* Initialize/cleanup HTTP client */
+void svc_init(void);
+void svc_cleanup(void);
+
+/* Submit a URL to all enabled archive services.
+ * Returns number of services that accepted the submission. */
+int svc_submit(const char *url, int services, int verbose);
+
+/* Submit to individual services (return 0 on success, -1 on failure) */
+int svc_submit_ia(const char *url, int verbose);
+int svc_submit_archiveph(const char *url, int verbose);
+int svc_submit_wikiwix(const char *url, int verbose);
+
+#endif /* SERVICES_H */
diff --git a/suploader.c b/suploader.c
@@ -0,0 +1,339 @@
+/* See LICENSE file for copyright and license details.
+ *
+ * suploader - Simple Uploader
+ *
+ * Uploads URLs to web archive services (Internet Archive,
+ * archive.today, Wikiwix). Reads URLs from a file or stdin.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "config.h"
+#include "services.h"
+#include "util.h"
+
+/* Global options */
+static int verbose = 0;
+static int daemon_mode = 0;
+static int delay_secs = DEFAULT_DELAY;
+static int random_order = 0;
+static int services = SVC_ALL;
+
+static void
+usage(void)
+{
+ fprintf(stderr,
+ "usage: suploader [-vrD] [-d secs] [-s services]"
+ " [file | -]\n"
+ "\n"
+ " -v verbose output\n"
+ " -r random URL selection order\n"
+ " -D daemon mode (loop forever)\n"
+ " -d secs delay between submissions"
+ " (default: %d)\n"
+ " -s services comma-separated: ia,archiveph,"
+ "wikiwix (default: all)\n"
+ "\n"
+ " file file with URLs, one per line\n"
+ " - read from stdin\n",
+ DEFAULT_DELAY);
+ exit(1);
+}
+
+/* Read all URLs from a file into an array.
+ * Returns array of strings, sets *count.
+ * Caller frees the array and each string. */
+static char **
+read_urls(const char *path, int *count)
+{
+ FILE *fp;
+ char line[MAX_URL_LEN];
+ char **urls;
+ int n, cap;
+ char *trimmed;
+
+ if (strcmp(path, "-") == 0)
+ fp = stdin;
+ else
+ fp = fopen(path, "r");
+
+ if (!fp)
+ die("cannot open: %s:", path);
+
+ cap = 256;
+ n = 0;
+ urls = xmalloc(cap * sizeof(char *));
+
+ while (fgets(line, sizeof(line), fp)) {
+ /* Strip newline */
+ line[strcspn(line, "\r\n")] = '\0';
+ trimmed = str_trim(line);
+
+ /* Skip empty lines and comments */
+ if (!*trimmed || *trimmed == '#')
+ continue;
+
+ /* Must look like a URL */
+ if (!str_starts_with(trimmed, "http://") &&
+ !str_starts_with(trimmed, "https://"))
+ continue;
+
+ if (n >= cap) {
+ cap *= 2;
+ urls = xrealloc(urls,
+ cap * sizeof(char *));
+ }
+ urls[n++] = xstrdup(trimmed);
+ }
+
+ if (fp != stdin)
+ fclose(fp);
+
+ *count = n;
+ return urls;
+}
+
+/* Write remaining URLs back to the file (for daemon mode) */
+static void
+write_urls(const char *path, char **urls, int count)
+{
+ FILE *fp;
+ int i;
+
+ if (strcmp(path, "-") == 0)
+ return;
+
+ fp = fopen(path, "w");
+ if (!fp) {
+ warn("cannot write: %s", path);
+ return;
+ }
+
+ for (i = 0; i < count; i++) {
+ if (urls[i])
+ fprintf(fp, "%s\n", urls[i]);
+ }
+
+ fclose(fp);
+}
+
+/* Remove an entry from the URL array by setting it to NULL */
+static void
+remove_url(char **urls, int idx)
+{
+ free(urls[idx]);
+ urls[idx] = NULL;
+}
+
+/* Count non-NULL entries */
+static int
+count_remaining(char **urls, int count)
+{
+ int i, n;
+
+ n = 0;
+ for (i = 0; i < count; i++) {
+ if (urls[i])
+ n++;
+ }
+ return n;
+}
+
+/* Pick a random non-NULL index */
+static int
+pick_random(char **urls, int count)
+{
+ int remaining, target, i;
+
+ remaining = count_remaining(urls, count);
+ if (remaining == 0)
+ return -1;
+
+ target = rand() % remaining;
+ for (i = 0; i < count; i++) {
+ if (urls[i]) {
+ if (target == 0)
+ return i;
+ target--;
+ }
+ }
+ return -1;
+}
+
+/* Pick next non-NULL index sequentially */
+static int
+pick_next(char **urls, int count)
+{
+ int i;
+
+ for (i = 0; i < count; i++) {
+ if (urls[i])
+ return i;
+ }
+ return -1;
+}
+
+/* Parse the -s services flag */
+static int
+parse_services(const char *arg)
+{
+ int svc;
+ char *copy, *tok, *saveptr;
+
+ svc = 0;
+ copy = xstrdup(arg);
+
+ for (tok = strtok_r(copy, ",", &saveptr); tok;
+ tok = strtok_r(NULL, ",", &saveptr)) {
+ tok = str_trim(tok);
+ if (strcmp(tok, "ia") == 0)
+ svc |= SVC_IA;
+ else if (strcmp(tok, "archiveph") == 0)
+ svc |= SVC_ARCHIVEPH;
+ else if (strcmp(tok, "wikiwix") == 0)
+ svc |= SVC_WIKIWIX;
+ else if (strcmp(tok, "all") == 0)
+ svc = SVC_ALL;
+ else
+ die("unknown service: %s", tok);
+ }
+
+ free(copy);
+ return svc ? svc : SVC_ALL;
+}
+
+int
+main(int argc, char *argv[])
+{
+ const char *file;
+ char **urls;
+ int count, idx, ok, processed, opt;
+ time_t start;
+
+ while ((opt = getopt(argc, argv, "vrDd:s:h")) != -1) {
+ switch (opt) {
+ case 'v':
+ verbose = 1;
+ break;
+ case 'r':
+ random_order = 1;
+ break;
+ case 'D':
+ daemon_mode = 1;
+ break;
+ case 'd':
+ delay_secs = atoi(optarg);
+ if (delay_secs < 1)
+ delay_secs = 1;
+ break;
+ case 's':
+ services = parse_services(optarg);
+ break;
+ case 'h': /* fallthrough */
+ default:
+ usage();
+ }
+ }
+
+ if (optind >= argc)
+ usage();
+
+ file = argv[optind];
+ srand(time(NULL));
+
+ fprintf(stderr, "%s %s\n", PROG_NAME, PROG_VERSION);
+ if (services & SVC_IA)
+ fprintf(stderr, " service: Internet Archive\n");
+ if (services & SVC_ARCHIVEPH)
+ fprintf(stderr, " service: archive.ph\n");
+ if (services & SVC_WIKIWIX)
+ fprintf(stderr, " service: Wikiwix\n");
+ fprintf(stderr, " delay: %ds\n", delay_secs);
+ if (daemon_mode)
+ fprintf(stderr, " mode: daemon\n");
+ fprintf(stderr, "\n");
+
+ svc_init();
+
+ processed = 0;
+ start = time(NULL);
+
+ do {
+ urls = read_urls(file, &count);
+
+ if (count == 0) {
+ if (daemon_mode) {
+ if (verbose)
+ fprintf(stderr,
+ "queue empty, waiting..."
+ "\n");
+ sleep(delay_secs);
+ free(urls);
+ continue;
+ }
+ fprintf(stderr, "no URLs to process\n");
+ free(urls);
+ break;
+ }
+
+ fprintf(stderr, "loaded %d URL(s)\n", count);
+
+ while (count_remaining(urls, count) > 0) {
+ if (random_order)
+ idx = pick_random(urls, count);
+ else
+ idx = pick_next(urls, count);
+
+ if (idx < 0)
+ break;
+
+ fprintf(stderr, "[%d] %s\n",
+ processed + 1, urls[idx]);
+
+ ok = svc_submit(urls[idx], services,
+ verbose);
+ fprintf(stderr, " %d service(s) OK\n", ok);
+
+ remove_url(urls, idx);
+ processed++;
+
+ /* Write back remaining URLs */
+ if (daemon_mode)
+ write_urls(file, urls, count);
+
+ /* Rate limit */
+ if (count_remaining(urls, count) > 0 ||
+ daemon_mode)
+ sleep(delay_secs);
+
+ /* In non-daemon mode, process all URLs */
+ if (!daemon_mode)
+ continue;
+
+ /* In daemon mode, re-read file each cycle
+ * in case new URLs were appended */
+ break;
+ }
+
+ /* Free URL array */
+ {
+ int i;
+
+ for (i = 0; i < count; i++)
+ free(urls[i]);
+ free(urls);
+ }
+
+ } while (daemon_mode);
+
+ fprintf(stderr,
+ "\nDone: %d URLs processed in %lds\n",
+ processed, (long)(time(NULL) - start));
+
+ svc_cleanup();
+ return 0;
+}
diff --git a/util.c b/util.c
@@ -0,0 +1,90 @@
+/* See LICENSE file for copyright and license details. */
+
+#include <ctype.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "util.h"
+
+void
+die(const char *fmt, ...)
+{
+ va_list ap;
+
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+ if (fmt[0] && fmt[strlen(fmt) - 1] == ':') {
+ fputc(' ', stderr);
+ perror(NULL);
+ } else {
+ fputc('\n', stderr);
+ }
+ exit(1);
+}
+
+void
+warn(const char *fmt, ...)
+{
+ va_list ap;
+
+ va_start(ap, fmt);
+ fprintf(stderr, "warning: ");
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+ fputc('\n', stderr);
+}
+
+void *
+xmalloc(size_t size)
+{
+ void *p = malloc(size);
+
+ if (!p)
+ die("malloc:");
+ return p;
+}
+
+void *
+xrealloc(void *ptr, size_t size)
+{
+ void *p = realloc(ptr, size);
+
+ if (!p)
+ die("realloc:");
+ return p;
+}
+
+char *
+xstrdup(const char *s)
+{
+ char *p = strdup(s);
+
+ if (!p)
+ die("strdup:");
+ return p;
+}
+
+char *
+str_trim(char *str)
+{
+ char *end;
+
+ while (isspace((unsigned char)*str))
+ str++;
+ if (*str == '\0')
+ return str;
+ end = str + strlen(str) - 1;
+ while (end > str && isspace((unsigned char)*end))
+ end--;
+ end[1] = '\0';
+ return str;
+}
+
+int
+str_starts_with(const char *str, const char *prefix)
+{
+ return strncmp(str, prefix, strlen(prefix)) == 0;
+}
diff --git a/util.h b/util.h
@@ -0,0 +1,21 @@
+/* See LICENSE file for copyright and license details. */
+
+#ifndef UTIL_H
+#define UTIL_H
+
+#include <stddef.h>
+
+/* Memory allocation with error handling */
+void *xmalloc(size_t size);
+void *xrealloc(void *ptr, size_t size);
+char *xstrdup(const char *s);
+
+/* String utilities */
+char *str_trim(char *str);
+int str_starts_with(const char *str, const char *prefix);
+
+/* Error handling */
+void die(const char *fmt, ...);
+void warn(const char *fmt, ...);
+
+#endif /* UTIL_H */