Apophis
view release on metacpan or search on metacpan
/*
* Windows compatibility:
* perl.h on threaded Windows redefines mkdir, stat, unlink etc. as macros
* requiring the interpreter context (my_perl). Our static C helper functions
* don't have pTHX, so we undo those overrides and use the real libc calls.
*/
#ifdef WIN32
# undef mkdir
# undef stat
# undef unlink
# undef rename
# undef open
# undef close
# undef read
# undef write
/* Windows mkdir takes only one arg */
# define apophis_mkdir(p, m) _mkdir(p)
/*
* Stat_t is struct w32_stat, the layout perl's own stat() fills in - but
* that stat() is the macro undef'd just above, so what is left is the CRT
* one, which writes a differently shaped struct. Pair the call and the
*/
# define apophis_stat_t struct _stat64
# define apophis_stat(p, b) _stat64((p), (b))
#else
# define apophis_mkdir(p, m) mkdir(p, m)
# define apophis_stat_t struct stat
# define apophis_stat(p, b) stat((p), (b))
#endif
/*
* The CRT rename() refuses a destination that already exists, so the second
* store of the same content with metadata could not replace its sidecar.
* MoveFileEx replaces, and stays atomic.
*/
static int
apophis_rename(const char *from, const char *to)
{
#ifdef WIN32
if (MoveFileExA(from, to, MOVEFILE_REPLACE_EXISTING))
return 0;
errno = EACCES;
return -1;
#else
return rename(from, to);
#endif
}
/* Horus UUID library - pure C, no Perl deps */
#define HORUS_FATAL(msg) croak("%s", (msg))
#include "horus_core.h"
/* ------------------------------------------------------------------ */
/* Constants */
/* ------------------------------------------------------------------ */
memcpy(buf, file_path, len + 1);
last_slash = strrchr(buf, '/');
if (last_slash) {
*last_slash = '\0';
apophis_mkdir_p(buf);
}
}
/* ------------------------------------------------------------------ */
/* Internal: atomic file write (temp + rename) */
/* ------------------------------------------------------------------ */
static void
apophis_atomic_write(pTHX_ const char *path,
const char *content, STRLEN content_len)
{
char tmp_path[APOPHIS_PATH_MAX];
PerlIO *fh;
SSize_t written;
croak("Apophis: cannot write '%s': %s", tmp_path, strerror(errno));
written = PerlIO_write(fh, content, content_len);
PerlIO_close(fh);
if (written != (SSize_t)content_len) {
unlink(tmp_path);
croak("Apophis: short write to '%s'", tmp_path);
}
if (apophis_rename(tmp_path, path) != 0) {
unlink(tmp_path);
croak("Apophis: cannot rename '%s' -> '%s': %s",
tmp_path, path, strerror(errno));
}
}
/* ------------------------------------------------------------------ */
/* Internal: metadata sidecar (key=value\n format) */
/* ------------------------------------------------------------------ */
static void
apophis_meta_write(pTHX_ const char *meta_path, HV *meta)
STRLEN vlen;
const char *vstr = SvPV(val, vlen);
PerlIO_write(fh, key, (SSize_t)klen);
PerlIO_write(fh, "=", 1);
PerlIO_write(fh, vstr, (SSize_t)vlen);
PerlIO_write(fh, "\n", 1);
}
PerlIO_close(fh);
if (apophis_rename(tmp_path, meta_path) != 0) {
unlink(tmp_path);
croak("Apophis: cannot rename metadata '%s': %s",
tmp_path, strerror(errno));
}
}
static HV *
apophis_meta_read(pTHX_ const char *meta_path)
{
PerlIO *fh;
HV *meta;
apophis_stat_t st;
*
* Stack input: self_sv, content_ref_sv
* Stack output: uuid_string_sv
*
* Fuses the entire store pipeline into a single op:
* 1. Extract namespace bytes from object
* 2. SHA-1 hash content â UUID v5
* 3. Compute 2-level sharded path
* 4. stat() for CAS dedup check
* 5. mkdir -p parent directories
* 6. Atomic write (temp + rename)
* 7. Format and return UUID string
*/
static OP *
pp_apophis_store(pTHX) {
dSP;
SV *content_ref_sv = POPs;
SV *self_sv = POPs;
HV *hv;
const unsigned char *ns;
SV *content_sv;
Revision history for Apophis
0.06 2026-08-31
- Move Apophis.xs to the top of the dist and drop the XSMULTI
build, which dlltool could not link on Strawberry.
- Pair the stat call with its struct on Windows.
- Replace an existing file on rename on Windows.
0.05 2026-08-20
- ADDS: a public C ABI, ap_abi.h, reached through
ExtUtils::Depends and Apophis::_abi_ptr.
- Regenerate ppport.h with Devel::PPPort 3.73. The old copy
undef'd perl's STMT_START/STMT_END to force the gcc
brace-group form, which is what every XopENTRY_set warned
about under clang. 102 warnings, none of them ours.
0.04 2026-07-11
horus_stamp_version_variant(uuid, 5);
```
### `apophis_path_for(store_dir, id) -> path string`
2-level hex sharding: `a3bb189e-...` â `store/a3/bb/a3bb189e-...`
### `apophis_mkdir_p(path)`
Recursive directory creation in C using `mkdir()`.
### `apophis_store_file(path, content, content_len)`
Atomic write: write to `path.tmp.$$`, then `rename()`.
### `apophis_meta_write(path, meta_hv)`
Simple key=value sidecar format (no JSON dependency). Writes `path.meta`.
### `apophis_meta_read(path) -> HV*`
Reads `.meta` sidecar back into a hash.
## XSUBs (Perl-visible API)
### `Apophis->new(namespace => $ns, store_dir => $dir)`
### 2-Level Hex Sharding
```
/store/a3/bb/a3bb189e-8bf9-5f18-b3f6-1b2f5f5c1e3a
```
256 x 256 = 65,536 shard directories. ~15 entries per leaf at 1M objects.
### Atomic Writes
```
write to: /store/a3/bb/a3bb189e-...tmp.PID
rename: /store/a3/bb/a3bb189e-...
```
### Metadata Sidecars
```
/store/a3/bb/a3bb189e-....meta
```
Simple `key=value\n` format. No JSON dependency.
## Implementation Phases
include/ap_abi.h view on Meta::CPAN
* check; a consumer should.
*
* A consumer must never reimplement this. The layout is Apophis's to
* change, and a second copy of the sharding rule means the day it changes
* every blob already on disk becomes unreachable through the consumer
* while staying perfectly findable through Apophis. */
int (*build_path)(char *out, size_t out_size,
const char *dir, STRLEN dirlen,
const char *id, STRLEN id_len);
/* Create the parent directory if needed, then write via temp-and-rename,
* which is what makes a store idempotent under concurrency. Croaks on an
* I/O error, the way Apophis's own store does - it is not a probe. */
void (*write_atomic)(pTHX_ const char *path,
const char *content, STRLEN len);
} ap_abi;
#endif /* AP_ABI_H */
lib/Apophis.pm view on Meta::CPAN
Apophis is a XS content addressable storage library built on the
B<Horus> UUID library (RFC 9562). It generates deterministic UUID v5
identifiers for arbitrary content using SHA-1 namespace hashing.
Same content always produces the same UUID. Different namespaces produce
different UUIDs for the same content.
Stored objects are sharded across a 2-level hex directory tree (65,536
directories) for efficient filesystem access at scale. Writes are atomic
(temp + rename). CAS is naturally idempotent â no locking required.
=head1 METHODS
=head2 new
my $ca = Apophis->new(
namespace => 'myapp-files', # required
store_dir => '/var/store', # optional default store path
);
lib/Apophis.pm view on Meta::CPAN
C<identify()> would for the same content.
=head2 store
my $id = $ca->store(\$content);
my $id = $ca->store(\$content, store_dir => '/other');
my $id = $ca->store(\$content, meta => { mime_type => 'image/png' });
Identifies the content and writes it to the sharded store. Returns the
UUID. If the content already exists, returns immediately (CAS dedup).
Writes are atomic via temp file + rename.
=head2 fetch
my $data_ref = $ca->fetch($id);
Returns a scalar reference to the stored content, or C<undef> if not found.
=head2 exists
if ($ca->exists($id)) { ... }
KEY_qx|5.003007||Viu
KEY_rand|5.003007||Viu
KEY_read|5.003007||Viu
KEY_readdir|5.003007||Viu
KEY_readline|5.003007||Viu
KEY_readlink|5.003007||Viu
KEY_readpipe|5.003007||Viu
KEY_recv|5.003007||Viu
KEY_redo|5.003007||Viu
KEY_ref|5.003007||Viu
KEY_rename|5.003007||Viu
KEY_require|5.003007||Viu
KEY_reset|5.003007||Viu
KEY_return|5.003007||Viu
KEY_reverse|5.003007||Viu
KEY_rewinddir|5.003007||Viu
KEY_rindex|5.003007||Viu
KEY_rmdir|5.003007||Viu
KEY_s|5.003007||Viu
KEY_say|5.009003||Viu
KEY_scalar|5.003007||Viu
M_PAT_MODS|5.009005||Viu
MPH_BUCKETS|5.027011||Viu
MPH_RSHIFT|5.027011||Viu
MPH_VALt|5.027011||Viu
mPUSHi|5.009002|5.003007|p
mPUSHn|5.009002|5.003007|p
mPUSHp|5.009002|5.003007|p
mPUSHs|5.010001|5.003007|p
mPUSHu|5.009002|5.003007|p
mro_clean_isarev|5.013007||Viu
mro_gather_and_rename|5.013007||Viu
mro_get_from_name|||u
mro_get_linear_isa|5.009005|5.009005|
mro_get_linear_isa_c3|||i
mro_get_linear_isa_dfs|5.009005||Vi
MRO_GET_PRIVATE_DATA|5.010001|5.010001|
mro_get_private_data|||cu
mro_isa_changed_in|5.009005||Vi
mro_meta_dup|5.009005||Viu
mro_meta_init|||ciu
mro_method_changed_in|5.009005|5.009005|
PerlLIO_link|5.006000||Viu
PerlLIO_lseek|5.005000||Viu
PerlLIO_lstat|5.005000||Viu
PerlLIO_mktemp|5.005000||Viu
PerlLIO_open3|5.005000||Viu
PerlLIO_open3_cloexec|5.027008||Viu
PerlLIO_open|5.005000||Viu
PerlLIO_open_cloexec|5.027008||Viu
PerlLIO_read|5.005000||Viu
PerlLIO_readlink|5.033005||Viu
PerlLIO_rename|5.005000||Viu
PerlLIO_setmode|5.005000||Viu
PerlLIO_stat|5.005000||Viu
PerlLIO_symlink|5.033005||Viu
PerlLIO_tmpnam|5.005000||Viu
PerlLIO_umask|5.005000||Viu
PerlLIO_unlink|5.005000||Viu
PerlLIO_utime|5.005000||Viu
PerlLIO_write|5.005000||Viu
PERL_LOADMOD_DENY|5.006000|5.003007|
PERL_LOADMOD_IMPORT_OPS|5.006000|5.003007|
reg_temp_copy|5.009005||cViu
REG_TOP_LEVEL_BRANCHES_SEEN|5.019009||Viu
regtry|5.005000||Viu
REG_UNBOUNDED_QUANTIFIER_SEEN|5.019009||Viu
REG_UNFOLDED_MULTI_SEEN|5.019009||Viu
REG_VERBARG_SEEN|5.019009||Viu
REG_ZERO_LEN_SEEN|5.019009||Viu
re_indentf|5.023009||vViu
re_intuit_start|5.006000||cVu
re_intuit_string|5.006000||cVu
rename|5.005000||Viu
Renew|5.003007|5.003007|
Renewc|5.003007|5.003007|
RENUM|5.005000||Viu
RENUM_t8|5.035004||Viu
RENUM_t8_p8|5.033003||Viu
RENUM_t8_pb|5.033003||Viu
RENUM_tb|5.035004||Viu
RENUM_tb_p8|5.033003||Viu
RENUM_tb_pb|5.033003||Viu
re_op_compile|5.017001||Viu
return IS_NUMBER_IN_UV;
}
return 0;
}
#endif
#endif
/*
* The grok_* routines have been modified to use warn() instead of
* Perl_warner(). Also, 'hexdigit' was the former name of PL_hexdigit,
* which is why the stack variable has been renamed to 'xdigit'.
*/
#ifndef grok_bin
#if defined(NEED_grok_bin)
static UV DPPP_(my_grok_bin)(pTHX_ const char * start, STRLEN * len_p, I32 * flags, NV * result);
static
#else
extern UV DPPP_(my_grok_bin)(pTHX_ const char * start, STRLEN * len_p, I32 * flags, NV * result);
#endif
( run in 1.014 second using v1.01-cache-2.11-cpan-a49fcb8fa48 )