@@ -14,7 +14,8 @@ import {
1414
1515const PREFIX = "terrariumctl backup export" ;
1616const DEFAULT_CONFIG_PATH = process . env . TERRARIUM_CONFIG_PATH ?? "/etc/terrarium/config.yaml" ;
17- const STATE_DIR = "/var/lib/terrarium" ;
17+ const STATE_DIR = process . env . TERRARIUM_STATE_DIR ?? "/var/lib/terrarium" ;
18+ const S3_SNAPSHOT_STATE_FORMAT = "zfs-recursive-v1" ;
1819const S3_EXPORT_ATTEMPTS = 4 ;
1920const S3_EXPORT_RETRY_MS = 5000 ;
2021
@@ -65,6 +66,21 @@ export function zfsReplicationSendCommand(latestSnapshot: string, parentSnapshot
6566 return `zfs send -R ${ shellEscape ( latestSnapshot ) } ` ;
6667}
6768
69+ export function planS3SnapshotExport (
70+ latestSnapshot : string ,
71+ lastSnapshot : string ,
72+ stateFormat : string ,
73+ parentSnapshotExists : boolean
74+ ) : { skip : boolean ; parentSnapshot : string ; full : boolean } {
75+ const recursiveState = stateFormat === S3_SNAPSHOT_STATE_FORMAT ;
76+ if ( recursiveState && lastSnapshot === latestSnapshot ) {
77+ return { skip : true , parentSnapshot : "" , full : false } ;
78+ }
79+
80+ const parentSnapshot = recursiveState && lastSnapshot && parentSnapshotExists ? lastSnapshot : "" ;
81+ return { skip : false , parentSnapshot, full : ! parentSnapshot } ;
82+ }
83+
6884export function isRetriableS3ExportError ( message : string ) : boolean {
6985 const lowered = message . toLowerCase ( ) ;
7086 return [
@@ -154,8 +170,15 @@ export async function backupExportCmd(configPath = DEFAULT_CONFIG_PATH): Promise
154170 }
155171
156172 const stateFile = join ( STATE_DIR , "lastsnapshots" , `${ instance . name } .txt` ) ;
173+ const stateFormatFile = join ( STATE_DIR , "lastsnapshots" , `${ instance . name } .format` ) ;
157174 const last = existsSync ( stateFile ) ? readFileSync ( stateFile , "utf8" ) . trim ( ) : "" ;
158- if ( last === latest ) {
175+ const stateFormat = existsSync ( stateFormatFile ) ? readFileSync ( stateFormatFile , "utf8" ) . trim ( ) : "" ;
176+ const parentSnapshotExists =
177+ stateFormat === S3_SNAPSHOT_STATE_FORMAT && last
178+ ? ( await runAllowFailure ( [ "zfs" , "list" , "-H" , "-t" , "snapshot" , last ] ) ) . exitCode === 0
179+ : false ;
180+ const exportPlan = planS3SnapshotExport ( latest , last , stateFormat , parentSnapshotExists ) ;
181+ if ( exportPlan . skip ) {
159182 continue ;
160183 }
161184
@@ -166,10 +189,7 @@ export async function backupExportCmd(configPath = DEFAULT_CONFIG_PATH): Promise
166189 const manifestPath = join ( manifestDir , `${ snapName } .json` ) ;
167190 mkdirSync ( manifestDir , { recursive : true } ) ;
168191
169- const streamSource =
170- last && ( await runAllowFailure ( [ "zfs" , "list" , "-H" , "-t" , "snapshot" , last ] ) ) . exitCode === 0
171- ? zfsReplicationSendCommand ( latest , last )
172- : zfsReplicationSendCommand ( latest ) ;
192+ const streamSource = zfsReplicationSendCommand ( latest , exportPlan . parentSnapshot ) ;
173193
174194 await runRetriableS3Command (
175195 `${ streamSource } | zstd -T0 | ${ awsBase . map ( shellEscape ) . join ( " " ) } s3 cp - ${ shellEscape ( `s3://${ bucket } /${ objectKey } ` ) } ` ,
@@ -181,9 +201,9 @@ export async function backupExportCmd(configPath = DEFAULT_CONFIG_PATH): Promise
181201 instance : instance . name ,
182202 dataset,
183203 snapshot : latest ,
184- parent_snapshot : last ,
204+ parent_snapshot : exportPlan . parentSnapshot ,
185205 object_key : objectKey ,
186- full : ! last ,
206+ full : exportPlan . full ,
187207 created_at : new Date ( ) . toISOString ( )
188208 } ;
189209 writeJsonFile ( manifestPath , manifest ) ;
@@ -193,5 +213,6 @@ export async function backupExportCmd(configPath = DEFAULT_CONFIG_PATH): Promise
193213 `S3 manifest upload for ${ instance . name } @${ snapName } `
194214 ) ;
195215 writeFileSync ( stateFile , `${ latest } \n` , "utf8" ) ;
216+ writeFileSync ( stateFormatFile , `${ S3_SNAPSHOT_STATE_FORMAT } \n` , "utf8" ) ;
196217 }
197218}
0 commit comments