2828import javax .sql .DataSource ;
2929import java .io .*;
3030import java .net .ServerSocket ;
31+ import java .nio .charset .StandardCharsets ;
3132import java .nio .ByteBuffer ;
3233import java .nio .channels .*;
3334import java .nio .file .*;
@@ -71,6 +72,7 @@ public class EmbeddedPostgres implements Closeable {
7172 private static final String PG_SUPERUSER = "postgres" ;
7273 private static final Duration DEFAULT_PG_STARTUP_WAIT = Duration .ofSeconds (10 );
7374 private static final String LOCK_FILE_NAME = "epg-lock" ;
75+ private static final String OWNERSHIP_MARKER_FILE_NAME = ".embedded-postgres-owned" ;
7476
7577 private final Path pgDir ;
7678
@@ -121,19 +123,20 @@ public class EmbeddedPostgres implements Closeable {
121123 Objects .requireNonNull (this .pgStartupWait , "Wait time cannot be null" );
122124
123125 if (parentDirectory != null ) {
124- mkdirs (parentDirectory );
126+ makeDirectories (parentDirectory );
125127 cleanOldDataDirectories (parentDirectory );
126128 this .dataDirectory = Objects .requireNonNullElseGet (dataDirectory , () -> parentDirectory .resolve (instanceId .toString ()));
127129 } else {
128130 this .dataDirectory = dataDirectory ;
129131 }
130132 if (this .dataDirectory == null ) throw new IllegalArgumentException ("no data directory" );
131133 LOG .trace ("{} postgres data directory is {}" , instanceId , this .dataDirectory );
132- mkdirs (this .dataDirectory );
134+ makeDirectories (this .dataDirectory );
135+ markDataDirectoryAsOwned ();
133136
134137 final Path lockFile = this .dataDirectory .resolve (LOCK_FILE_NAME );
135138
136- if (cleanDataDirectory || Files .notExists (this .dataDirectory .resolve ("postgresql.conf" ))) initdb ();
139+ if (cleanDataDirectory || Files .notExists (this .dataDirectory .resolve ("postgresql.conf" ))) initDb ();
137140
138141 this .lockChannel = FileChannel .open (lockFile , StandardOpenOption .CREATE , StandardOpenOption .WRITE );
139142 this .lock = this .lockChannel .tryLock ();
@@ -146,7 +149,7 @@ public class EmbeddedPostgres implements Closeable {
146149 throw new IllegalStateException ("could not lock " + lockFile );
147150 }
148151
149- if (dataDirectoryCustomizer != null ) dataDirectoryCustomizer .accept (dataDirectory );
152+ if (dataDirectoryCustomizer != null ) dataDirectoryCustomizer .accept (this . dataDirectory );
150153
151154 startPostmaster ();
152155 }
@@ -268,14 +271,41 @@ private static String formatElapsedSince(final Instant instant) {
268271 return String .format ("%02d:%02d:%02d.%03d" , hours , minutes , seconds , millis );
269272 }
270273
271- private void initdb () {
274+ private static String ownershipMarkerNameFor (final Path dataDirectory ) {
275+ final Path normalizedPath = dataDirectory .toAbsolutePath ().normalize ();
276+ try {
277+ final MessageDigest messageDigest = MessageDigest .getInstance ("SHA-256" );
278+ final byte [] hash = messageDigest .digest (normalizedPath .toString ().getBytes (StandardCharsets .UTF_8 ));
279+ return OWNERSHIP_MARKER_FILE_NAME + "-" + HexFormat .of ().formatHex (hash );
280+ } catch (final NoSuchAlgorithmException e ) {
281+ throw new IllegalStateException ("SHA-256 algorithm is not available" , e );
282+ }
283+ }
284+
285+ private static Path ownershipMarkerPathFor (final Path dataDirectory ) {
286+ final Path normalizedPath = dataDirectory .toAbsolutePath ().normalize ();
287+ final Path parentDirectory = normalizedPath .getParent ();
288+ if (parentDirectory == null ) throw new IllegalStateException ("could not determine parent directory for " + normalizedPath );
289+ return parentDirectory .resolve (ownershipMarkerNameFor (normalizedPath ));
290+ }
291+
292+ private void markDataDirectoryAsOwned () {
293+ final Path ownershipMarker = ownershipMarkerPathFor (dataDirectory );
294+ try {
295+ Files .writeString (ownershipMarker , dataDirectory .toAbsolutePath ().normalize ().toString (), StandardOpenOption .CREATE , StandardOpenOption .TRUNCATE_EXISTING , StandardOpenOption .WRITE );
296+ } catch (final IOException e ) {
297+ throw new IllegalStateException ("could not create ownership marker " + ownershipMarker , e );
298+ }
299+ }
300+
301+ private void initDb () {
272302 final Instant start = Instant .now ();
273303 final List <String > args = new ArrayList <>(Arrays .asList (
274304 "-A" , "trust" , "-U" , PG_SUPERUSER ,
275305 "-D" , dataDirectory .toString (), "-E" , "UTF-8" ));
276306 args .addAll (createLocaleOptions ());
277307 system (initDb , args , null );
278- if (LOG .isInfoEnabled ()) LOG .info ("{} initdb completed in {}" , instanceId , formatElapsedSince (start ));
308+ if (LOG .isInfoEnabled ()) LOG .info ("{} initDb completed in {}" , instanceId , formatElapsedSince (start ));
279309 }
280310
281311 private void startPostmaster () {
@@ -375,6 +405,7 @@ public void close() throws IOException {
375405 if (cleanDataDirectory && System .getProperty ("ot.epg.no-cleanup" ) == null ) {
376406 try {
377407 deleteDirectoryRecursively (dataDirectory );
408+ Files .deleteIfExists (ownershipMarkerPathFor (dataDirectory ));
378409 } catch (final IOException _) {
379410 LOG .error ("Could not clean up directory {}" , dataDirectory .toAbsolutePath ());
380411 }
@@ -406,37 +437,79 @@ private void pgCtl(final Path dir, final String action) {
406437 system (pgCtl , args , null );
407438 }
408439
409- @ SuppressWarnings ("java:S1141" )
440+ @ SuppressWarnings ({ "java:S1141" , "java:S1192" } )
410441 private void cleanOldDataDirectories (final Path parentDirectory ) {
442+ final Path normalizedParentDirectory = parentDirectory .toAbsolutePath ().normalize ();
411443 try (final Stream <Path > children = Files .list (parentDirectory )) {
412- for (final Path dir : children .toList ()) {
413- if (!Files .isDirectory (dir )) continue ;
444+ for (final Path ownershipMarker : children .filter (Files ::isRegularFile ).toList ()) {
445+ final String fileName = ownershipMarker .getFileName ().toString ();
446+ if (!fileName .startsWith (OWNERSHIP_MARKER_FILE_NAME + "-" )) continue ;
447+
448+ final Path dir ;
449+ try {
450+ final String content = Files .readString (ownershipMarker ).trim ();
451+ if (content .isEmpty ()) {
452+ Files .deleteIfExists (ownershipMarker );
453+ continue ;
454+ }
455+ dir = Path .of (content ).toAbsolutePath ().normalize ();
456+ } catch (final Exception e ) {
457+ LOG .warn ("Could not read ownership marker {}" , ownershipMarker , e );
458+ continue ;
459+ }
460+
461+ if (!Objects .equals (dir .getParent (), normalizedParentDirectory )) {
462+ LOG .warn ("Skipping ownership marker {} because it points outside {}" , ownershipMarker , normalizedParentDirectory );
463+ continue ;
464+ }
465+
466+ if (Files .notExists (dir )) {
467+ try {
468+ Files .deleteIfExists (ownershipMarker );
469+ } catch (final IOException e ) {
470+ LOG .warn ("Could not remove stale ownership marker {}" , ownershipMarker , e );
471+ }
472+ continue ;
473+ }
414474
415475 final Path theLockFile = dir .resolve (LOCK_FILE_NAME );
416- if (Files .notExists (theLockFile )) continue ;
476+ final Path freshnessFile = Files .exists (theLockFile ) ? theLockFile : ownershipMarker ;
477+ if (System .currentTimeMillis () - Files .getLastModifiedTime (freshnessFile ).toMillis () < 10 * 60 * 1000 ) continue ;
478+
479+ boolean canDelete = false ;
480+ if (Files .exists (theLockFile )) {
481+ try (final FileChannel fileChannel = FileChannel .open (theLockFile , StandardOpenOption .CREATE , StandardOpenOption .WRITE );
482+ final FileLock theLock = fileChannel .tryLock ()) {
483+ if (theLock != null ) canDelete = true ;
484+ } catch (final OverlappingFileLockException e ) {
485+ LOG .trace ("While cleaning old data directories" , e );
486+ continue ;
487+ } catch (final Exception e ) {
488+ LOG .warn ("While cleaning old data directories" , e );
489+ continue ;
490+ }
491+ } else {
492+ canDelete = true ;
493+ }
417494
418- if (System . currentTimeMillis () - Files . getLastModifiedTime ( theLockFile ). toMillis () < 10 * 60 * 1000 ) continue ;
495+ if (! canDelete ) continue ;
419496
420- try (final FileChannel fileChannel = FileChannel .open (theLockFile , StandardOpenOption .CREATE , StandardOpenOption .WRITE );
421- final FileLock theLock = fileChannel .tryLock ()) {
422- if (theLock != null ) {
423- LOG .info ("Found stale data directory {}" , dir );
424- if (Files .exists (dir .resolve ("postmaster.pid" ))) {
425- try {
426- pgCtl (dir , "stop" );
427- LOG .info ("Shut down orphaned postmaster!" );
428- } catch (final Exception e ) {
429- if (LOG .isDebugEnabled ()) {
430- LOG .warn ("Failed to stop postmaster {}" , dir , e );
431- } else {
432- LOG .warn ("Failed to stop postmaster {}: {}" , dir , e .getMessage ());
433- }
434- }
497+ LOG .info ("Found stale data directory {}" , dir );
498+ if (Files .exists (dir .resolve ("postmaster.pid" ))) {
499+ try {
500+ pgCtl (dir , "stop" );
501+ LOG .info ("Shut down orphaned postmaster!" );
502+ } catch (final Exception e ) {
503+ if (LOG .isDebugEnabled ()) {
504+ LOG .warn ("Failed to stop postmaster {}" , dir , e );
505+ } else {
506+ LOG .warn ("Failed to stop postmaster {}: {}" , dir , e .getMessage ());
435507 }
436- deleteDirectoryRecursively (dir );
437508 }
438- } catch (final OverlappingFileLockException e ) {
439- LOG .trace ("While cleaning old data directories" , e );
509+ }
510+ try {
511+ deleteDirectoryRecursively (dir );
512+ Files .deleteIfExists (ownershipMarker );
440513 } catch (final Exception e ) {
441514 LOG .warn ("While cleaning old data directories" , e );
442515 }
@@ -447,6 +520,7 @@ private void cleanOldDataDirectories(final Path parentDirectory) {
447520 }
448521
449522
523+
450524 private static Path getWorkingDirectory () {
451525 return Path .of (System .getProperty ("ot.epg.working-dir" ,
452526 Path .of (System .getProperty ("java.io.tmpdir" )).resolve ("embedded-pg" ).toString ()));
@@ -690,7 +764,10 @@ public Builder setDataDirectoryCustomizer(final Consumer<Path> dataDirectoryCust
690764 */
691765 public EmbeddedPostgres start () throws IOException {
692766 if (builderPort == 0 ) builderPort = detectFreePort ();
693- if (builderDataDirectory == null ) builderDataDirectory = Files .createTempDirectory ("epg" );
767+ if (builderDataDirectory == null ) {
768+ Files .createDirectories (parentDirectory );
769+ builderDataDirectory = Files .createTempDirectory (parentDirectory , "epg-" );
770+ }
694771 return new EmbeddedPostgres (parentDirectory , builderDataDirectory , builderCleanDataDirectory , builderRegisterShutdownHook , config ,
695772 localeConfig , builderPort , connectConfig , pgBinaryResolver , errRedirector , outRedirector ,
696773 pgStartupWait , overrideWorkingDirectory , dataDirectoryCustomizer );
@@ -765,7 +842,7 @@ private void system(final Command command, final List<String> args, final Durati
765842 }
766843 }
767844
768- private static void mkdirs (final Path dir ) {
845+ private static void makeDirectories (final Path dir ) {
769846 try {
770847 Files .createDirectories (dir );
771848 } catch (final IOException e ) {
@@ -834,7 +911,7 @@ private static void extractTxz(final InputStream stream, final Path targetDir) t
834911 final byte [] content = new byte [(int ) entry .getSize ()];
835912 final int read = tarIn .read (content , 0 , content .length );
836913 if (read == -1 ) throw new IllegalStateException ("could not read " + individualFile );
837- mkdirs (fsObject .getParent ());
914+ makeDirectories (fsObject .getParent ());
838915
839916 final AsynchronousFileChannel fileChannel = AsynchronousFileChannel .open (fsObject , CREATE , WRITE );
840917 final ByteBuffer buffer = ByteBuffer .wrap (content );
@@ -863,7 +940,7 @@ private void closeChannel(final Channel channel) {
863940 }
864941 });
865942 } else if (entry .isDirectory ()) {
866- mkdirs (fsObject );
943+ makeDirectories (fsObject );
867944 } else {
868945 throw new UnsupportedOperationException (String .format ("Unsupported entry found: %s" , individualFile ));
869946 }
@@ -910,14 +987,14 @@ private static Path prepareBinaries(final PgBinaryResolver pgBinaryResolver, fin
910987 }
911988
912989 try (final DigestInputStream pgArchiveData = new DigestInputStream (pgBinary , MessageDigest .getInstance ("SHA-512" ));
913- final ByteArrayOutputStream baos = new ByteArrayOutputStream ()) {
914- pgArchiveData .transferTo (baos );
990+ final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream ()) {
991+ pgArchiveData .transferTo (byteArrayOutputStream );
915992
916993 final String pgDigest = HexFormat .of ().formatHex (pgArchiveData .getMessageDigest ().digest ());
917994 final Path workingDirectory = Optional .ofNullable (overrideWorkingDirectory ).orElse (getWorkingDirectory ());
918995 pgDir = workingDirectory .resolve (String .format ("PG-%s" , pgDigest ));
919996
920- mkdirs (pgDir );
997+ makeDirectories (pgDir );
921998
922999 final FileStore store = Files .getFileStore (workingDirectory );
9231000 if (store .supportsFileAttributeView (PosixFileAttributeView .class )) {
@@ -938,7 +1015,7 @@ private static Path prepareBinaries(final PgBinaryResolver pgBinaryResolver, fin
9381015 final FileLock unpackLock = fileChannel .tryLock ()) {
9391016 if (unpackLock != null ) {
9401017 LOG .info ("Extracting Postgres..." );
941- try (final ByteArrayInputStream bais = new ByteArrayInputStream (baos .toByteArray ())) {
1018+ try (final ByteArrayInputStream bais = new ByteArrayInputStream (byteArrayOutputStream .toByteArray ())) {
9421019 extractTxz (bais , pgDir );
9431020 }
9441021 if (Files .notExists (pgDirExists )) {
0 commit comments