This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 65d1d016d105ae4b0144db6fc2462cd13ca44b3a
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Wed Dec 18 14:53:03 2024 +0100

    In case of error while writing a GeoTIFF file, restore the file to its 
previous state.
---
 .../apache/sis/storage/geotiff/GeoTiffStore.java   |   1 -
 .../org/apache/sis/storage/geotiff/Writer.java     | 108 ++++++++++-----------
 .../sis/storage/geotiff/writer/TileMatrix.java     |  11 +--
 .../apache/sis/io/stream/ChannelDataOutput.java    |  28 ++++++
 4 files changed, 87 insertions(+), 61 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
index 5dabda1abe..34925a609e 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
@@ -699,7 +699,6 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
             final long offsetIFD;
             try {
                 offsetIFD = writer.append(image, grid, metadata);
-                writer.flush();
             } finally {
                 writer.synchronize(reader, true);
             }
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
index 43f7aa3d52..1ce0d8140c 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
@@ -146,13 +146,14 @@ final class Writer extends IOBase implements Flushable {
     int imageIndex;
 
     /**
-     * Offset where to write the next image, or {@code null} if writing a 
mandatory image (the first one).
+     * Offset where to write the offset of current image, or {@code null} when 
writing the first image of the file.
      * If null, the IFD offset is assumed already written and the {@linkplain 
#output} already at that position.
-     * Otherwise the value at the specified offset should be zero and will be 
updated if a new image is appended.
+     * Otherwise, the value at the specified offset is initially zero and will 
be updated after it become known.
      *
-     * @see #seekToNextImage()
+     * <p>After the image has been successfully written, this pointer is 
replaced by the pointer where the next
+     * image (if any) can be appended.</p>
      */
-    private UpdatableWrite<?> nextIFD;
+    private UpdatableWrite<?> currentIFD;
 
     /**
      * All values that couldn't be written immediately.
@@ -228,13 +229,13 @@ final class Writer extends IOBase implements Flushable {
     }
 
     /**
-     * Prepares the writer to write after the last images.
+     * Prepares the writer to write after the last image.
      *
      * @param  reader  the reader of images.
      */
     final void moveAfterExisting(final Reader reader) throws IOException, 
DataStoreException {
         Class<? extends Number> type = isBigTIFF ? Long.class : Integer.class;
-        nextIFD = UpdatableWrite.ofZeroAt(reader.offsetOfWritableIFD(), type);
+        currentIFD = UpdatableWrite.ofZeroAt(reader.offsetOfWritableIFD(), 
type);
         imageIndex = reader.getImageCacheSize();
     }
 
@@ -295,37 +296,46 @@ final class Writer extends IOBase implements Flushable {
             throws IOException, DataStoreException
     {
         final var exportable = new ReformattedImage(image, this::processor, 
anyTileSize);
-        final TileMatrix tiles;
+        /*
+         * Offset where the image IFD will start. The current version appends 
the new image at the end of file.
+         * A future version could perform a more extensive search for free 
space in the middle of the file.
+         * It could be useful when images have been deleted.
+         */
+        final long offsetIFD = output.length();
+        if (currentIFD != null) {
+            currentIFD.setAsLong(offsetIFD);
+            writeOrQueue(currentIFD);
+            output.seek(offsetIFD);
+        }
+        /*
+         * Write the Image File Directory (IFD) followed by the raster data.
+         */
         try {
-            tiles = writeImageFileDirectory(exportable, grid, metadata, false);
-        } finally {
-            largeTagData.clear();       // For making sure that there is no 
memory retention.
-        }
-        tiles.writeRasters(output);
-        wordAlign(output);
-        tiles.writeOffsetsAndLengths(output);
-        return tiles.offsetIFD;
-    }
-
-    /**
-     * Sets the {@linkplain #output} position to where to write the next image.
-     *
-     * @return offset where the image IFD will start. This is the {@link 
#output} position.
-     *
-     * @todo Current version append the new image at the end of file. A future 
version could perform a more extensive
-     *       search for free space in the middle of the file. It could be 
useful when images have been deleted.
-     */
-    private long seekToNextImage() throws IOException {
-        if (nextIFD == null) {
-            // `output` is already at the right position.
-            return output.getStreamPosition();
-        }
-        final long position = output.length();
-        nextIFD.setAsLong(position);
-        writeOrQueue(nextIFD);
-        output.seek(position);
-        nextIFD = null;
-        return position;
+            final TileMatrix tiles;
+            try {
+                tiles = writeImageFileDirectory(exportable, grid, metadata, 
false);
+            } finally {
+                largeTagData.clear();       // For making sure that there is 
no memory retention.
+            }
+            tiles.writeRasters(output);
+            wordAlign(output);
+            tiles.writeOffsetsAndLengths(output);
+            flush();
+            currentIFD = tiles.nextIFD;     // Set only after the operation 
succeeded.
+        } catch (Throwable e) {
+            try {
+                deferredWrites.clear();
+                output.truncate(offsetIFD);
+                if (currentIFD != null) {
+                    currentIFD.setAsLong(0);
+                    currentIFD.update(output);
+                }
+            } catch (Throwable more) {
+                e.addSuppressed(more);
+            }
+            throw e;
+        }
+        return offsetIFD;
     }
 
     /**
@@ -410,14 +420,13 @@ final class Writer extends IOBase implements Flushable {
          * If the image has any unsupported feature, the exception should have 
been thrown before this point.
          * Now start writing the entries. The entries in an IFD must be sorted 
in ascending order by tag code.
          */
-        output.flush();             // Makes room in the buffer for increasing 
our ability to modify past values.
+        output.flush();       // Make room in the buffer for increasing our 
ability to modify previous values.
         largeTagData.clear();
-        final long offsetIFD = seekToNextImage();
         final UpdatableWrite<?> tagCountWriter =
                 isBigTIFF ? UpdatableWrite.of(output, (long)  numberOfTags)
                           : UpdatableWrite.of(output, (short) numberOfTags);
 
-        final var tiling = new TileMatrix(image.exportable, numPlanes, 
bitsPerSample, offsetIFD,
+        final var tiling = new TileMatrix(image.exportable, numPlanes, 
bitsPerSample,
                                           compression.method, 
compression.level, compression.predictor);
         /*
          * Reminder: TIFF tags should be written in increasing numerical order.
@@ -472,7 +481,7 @@ final class Writer extends IOBase implements Flushable {
          */
         tagCountWriter.setAsLong(numberOfTags);
         writeOrQueue(tagCountWriter);
-        nextIFD = writeOffset(0);
+        tiling.nextIFD = writeOffset(0);
         for (final TagValue tag : largeTagData) {
             UpdatableWrite<?> offset = tag.writeHere(output);
             if (offset != null) deferredWrites.add(offset);
@@ -863,18 +872,6 @@ final class Writer extends IOBase implements Flushable {
         }
     }
 
-    /**
-     * Writes deferred values immediately to the output stream.
-     *
-     * @throws IOException if an error occurred while writing deferred data.
-     */
-    private void flushDeferredWrites() throws IOException {
-        UpdatableWrite<?> change;
-        while ((change = deferredWrites.pollFirst()) != null) {
-            change.update(output);
-        }
-    }
-
     /**
      * Sends to the writable channel any information that are still in buffers.
      * This method does not flush the writable channel itself.
@@ -883,8 +880,11 @@ final class Writer extends IOBase implements Flushable {
      */
     @Override
     public void flush() throws IOException {
-        flushDeferredWrites();
-        output.flush();
+        UpdatableWrite<?> change;
+        while ((change = deferredWrites.pollFirst()) != null) {
+            change.update(output);
+        }
+        output.flush();     // Flush buffer → channel, but not channel → disk.
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/TileMatrix.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/TileMatrix.java
index 52947d685b..0292e39dbd 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/TileMatrix.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/TileMatrix.java
@@ -33,6 +33,7 @@ import java.awt.image.DataBufferDouble;
 import java.awt.image.RasterFormatException;
 import java.awt.image.SampleModel;
 import org.apache.sis.image.DataType;
+import org.apache.sis.io.stream.UpdatableWrite;
 import org.apache.sis.io.stream.ChannelDataOutput;
 import org.apache.sis.io.stream.HyperRectangleWriter;
 import org.apache.sis.storage.DataStoreException;
@@ -51,9 +52,10 @@ import org.apache.sis.pending.jdk.JDK18;
  */
 public final class TileMatrix {
     /**
-     * Offset in {@link ChannelDataOutput} where the IFD starts.
+     * Where the next image will be written.
+     * This information is saved for the caller but not used by this class.
      */
-    public final long offsetIFD;
+    public UpdatableWrite<?> nextIFD;
 
     /**
      * The images to write.
@@ -121,17 +123,14 @@ public final class TileMatrix {
      * @param image             the image to write.
      * @param numPlanes         the number of banks (plane in TIFF 
terminology).
      * @param bitsPerSample     number of bits per sample to write.
-     * @param offsetIFD         offset in {@link ChannelDataOutput} where the 
IFD starts.
      * @param compression       the compression method to apply.
      * @param compressionLevel  compression level (0-9), or -1 for the default.
      * @param predictor         the predictor to apply before to compress data.
      */
     public TileMatrix(final RenderedImage image, final int numPlanes, final 
int[] bitsPerSample,
-                      final long offsetIFD, final Compression compression, 
final int compressionLevel,
-                      final Predictor predictor)
+                      final Compression compression, final int 
compressionLevel, final Predictor predictor)
     {
         final int pixelSize, numArrays;
-        this.offsetIFD        = offsetIFD;
         this.numPlanes        = numPlanes;
         this.image            = image;
         this.compression      = compression;
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataOutput.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataOutput.java
index dcaef76e99..557a1eec7b 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataOutput.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataOutput.java
@@ -787,6 +787,34 @@ public class ChannelDataOutput extends ChannelData 
implements DataOutput, Flusha
         }
     }
 
+    /**
+     * Truncates the stream to the given position. If the given size is 
greater than the current stream size,
+     * then this method does nothing. Otherwise this method set the stream 
size to the given size and, if the
+     * current position is greater than the new size, also set the stream to 
that position.
+     *
+     * @param  size  the position where to truncate.
+     * @throws IOException if the stream cannot be truncated to the given 
position.
+     */
+    public final void truncate(final long size) throws IOException {
+        if (channel instanceof SeekableByteChannel) {
+            // Unconditional truncate because more data may exist after 
current position.
+            ((SeekableByteChannel) 
channel).truncate(toSeekableByteChannelPosition(size));
+            if (size <= bufferOffset) {
+                bufferOffset = size;
+                bitPosition = 0;
+                buffer.limit(0);
+                return;
+            }
+        }
+        final long p = Math.subtractExact(size, bufferOffset);
+        if (p >= 0 && p <= buffer.limit()) {
+            buffer.limit((int) p);
+            bitPosition = 0;
+        } else {
+            throw new 
IOException(Resources.format(Resources.Keys.StreamIsForwardOnly_1, filename));
+        }
+    }
+
     /**
      * Moves to the given position in this stream.
      * If the given position is greater than the stream length, then the 
values of all bytes between

Reply via email to