This is an automated email from the ASF dual-hosted git repository.
tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git
The following commit(s) were added to refs/heads/main by this push:
new 3a9f5e4da7 TIKA-4989: file-system emitter can gzip its output;
tika-eval finds .… (#3384)
3a9f5e4da7 is described below
commit 3a9f5e4da7181cd37c8fde05b9aa06bd257107fa
Author: Tim Allison <[email protected]>
AuthorDate: Wed Oct 21 14:57:26 2026 -0400
TIKA-4989: file-system emitter can gzip its output; tika-eval finds .…
(#3384)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
CHANGES.txt | 4 +
.../ROOT/pages/pipes/plugins/filesystem.adoc | 4 +
.../org/apache/tika/eval/app/EvalFilePaths.java | 4 +-
.../org/apache/tika/eval/app/ProfilerBase.java | 7 +-
.../org/apache/tika/eval/app/io/ExtractReader.java | 32 ++++++-
.../tika/eval/app/ComparerGzipExtractsTest.java | 97 ++++++++++++++++++++++
.../tika/pipes/emitter/fs/FileSystemEmitter.java | 92 +++++++++++++++++---
.../pipes/emitter/fs/FileSystemEmitterConfig.java | 18 +++-
.../pipes/emitter/fs/FileSystemEmitterTest.java | 90 ++++++++++++++++++++
9 files changed, 328 insertions(+), 20 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 8a15e0a123..554ada5072 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,9 @@
Release 4.2.0 - unreleased
+ * The file-system emitter can gzip its output ("compression": "GZIP"), and
tika-eval
+ finds gzipped extracts (.json.gz) when comparing against a plain extract
set, measuring
+ their extract lengths and -m/-n limits on uncompressed size (TIKA-4989).
+
* tika-server activates catalog presets from the command line (--preset
<name>, repeatable)
and the environment (TIKA_PRESETS, comma-separated), the equivalent of
"<name>": true in
the config's presets, overriding the file's own entry for the name; a
config file needs
diff --git a/docs/modules/ROOT/pages/pipes/plugins/filesystem.adoc
b/docs/modules/ROOT/pages/pipes/plugins/filesystem.adoc
index c5000a594a..e1ec32d54a 100644
--- a/docs/modules/ROOT/pages/pipes/plugins/filesystem.adoc
+++ b/docs/modules/ROOT/pages/pipes/plugins/filesystem.adoc
@@ -147,6 +147,10 @@ Writes parsed results as files under `basePath`. The
relative output path is der
|`atomicWrites`
|`true`
|Write each output to a sibling `<name>.<uuid>.tmp` and rename it into place.
A reader of the output directory never sees a partial file, but it does see the
tmp files: crawlers and watchers over the output directory must ignore `*.tmp`.
A worker killed mid-write leaves its `.tmp` behind, and nothing sweeps them.
The `onExists: EXCEPTION`/`SKIP` check runs immediately before the rename and
is best-effort against two workers emitting the same key concurrently (last
writer wins) -- give wo [...]
+
+|`compression`
+|`NONE`
+|`GZIP` gzips each output and appends `.gz` to its name, after `fileExtension`
(`a.pdf.json.gz`). Applies where `fileExtension` does: the JSON metadata list,
and the text of `CONTENT_ONLY`; unpacked files are written as they are.
Extracted text typically shrinks 5-8x, and tika-eval reads `.json.gz` extracts
directly. Init-time only.
|===
[#file-system-iterator]
diff --git
a/tika-eval/tika-eval-app/src/main/java/org/apache/tika/eval/app/EvalFilePaths.java
b/tika-eval/tika-eval-app/src/main/java/org/apache/tika/eval/app/EvalFilePaths.java
index 8ac28e6ae6..e7e0c81dbe 100644
---
a/tika-eval/tika-eval-app/src/main/java/org/apache/tika/eval/app/EvalFilePaths.java
+++
b/tika-eval/tika-eval-app/src/main/java/org/apache/tika/eval/app/EvalFilePaths.java
@@ -23,6 +23,8 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
+import org.apache.tika.eval.app.io.ExtractReader;
+
/**
* Simple struct to keep track of relative path of source file (
* original binary file, e.g. /subdir/document1.doc)
@@ -45,7 +47,7 @@ class EvalFilePaths {
public EvalFilePaths(Path relativeSourceFilePath, Path extractFile) {
if (extractFile != null && Files.isRegularFile(extractFile)) {
try {
- extractFileLength = Files.size(extractFile);
+ extractFileLength = ExtractReader.extractLength(extractFile);
} catch (IOException e) {
//swallow ?
}
diff --git
a/tika-eval/tika-eval-app/src/main/java/org/apache/tika/eval/app/ProfilerBase.java
b/tika-eval/tika-eval-app/src/main/java/org/apache/tika/eval/app/ProfilerBase.java
index 7e68ad7e49..9ee2e01010 100644
---
a/tika-eval/tika-eval-app/src/main/java/org/apache/tika/eval/app/ProfilerBase.java
+++
b/tika-eval/tika-eval-app/src/main/java/org/apache/tika/eval/app/ProfilerBase.java
@@ -43,6 +43,7 @@ import org.xml.sax.SAXException;
import org.apache.tika.eval.app.db.ColInfo;
import org.apache.tika.eval.app.db.Cols;
import org.apache.tika.eval.app.db.TableInfo;
+import org.apache.tika.eval.app.io.ExtractReader;
import org.apache.tika.eval.app.io.ExtractReaderException;
import org.apache.tika.eval.app.io.IDBWriter;
import org.apache.tika.eval.app.io.PipesReport;
@@ -93,7 +94,7 @@ public abstract class ProfilerBase {
private static final String CONTAINER_EXCEPTION_1X = "X-TIKA" +
":EXCEPTION:runtime";
private static final Logger LOG =
LoggerFactory.getLogger(ProfilerBase.class);
private static final String[] EXTRACT_EXTENSIONS = {".json", ".txt", ""};
- private static final String[] COMPRESSION_EXTENSIONS = {"", ".bz2",
".gzip", ".zip",};
+ private static final String[] COMPRESSION_EXTENSIONS = {"", ".gz", ".bz2",
".gzip", ".zip",};
private static final String ZERO = "0";
private static final String UNKNOWN_EXTENSION = "unk";
//make this configurable
@@ -111,7 +112,7 @@ public abstract class ProfilerBase {
new ColInfo(Cols.BASE_MIME, Types.VARCHAR, 256),
new ColInfo(Cols.FILE_EXTENSION, Types.VARCHAR, 12));
private static CommonTokenCountManager COMMON_TOKEN_COUNT_MANAGER;
- private static Pattern FILE_NAME_CLEANER =
Pattern.compile("\\.(json|txt)(\\.(bz2|gz|zip))?$");
+ private static Pattern FILE_NAME_CLEANER =
Pattern.compile("\\.(json|txt)(\\.(bz2|gz|gzip|zip))?$");
private static LanguageIDWrapper LANG_ID = new LanguageIDWrapper();
protected IDBWriter writer;
AnalyzerManager analyzerManager;
@@ -867,7 +868,7 @@ public abstract class ProfilerBase {
protected long getFileLength(Path p) {
if (p != null && Files.isRegularFile(p)) {
try {
- return Files.size(p);
+ return ExtractReader.extractLength(p);
} catch (IOException e) {
//swallow
}
diff --git
a/tika-eval/tika-eval-app/src/main/java/org/apache/tika/eval/app/io/ExtractReader.java
b/tika-eval/tika-eval-app/src/main/java/org/apache/tika/eval/app/io/ExtractReader.java
index cfd2bc3db3..2f7436d76b 100644
---
a/tika-eval/tika-eval-app/src/main/java/org/apache/tika/eval/app/io/ExtractReader.java
+++
b/tika-eval/tika-eval-app/src/main/java/org/apache/tika/eval/app/io/ExtractReader.java
@@ -21,9 +21,13 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -93,6 +97,32 @@ public class ExtractReader {
return fileSuffixes;
}
+ /**
+ * Uncompressed length for gzip (from the ISIZE trailer), so plain and
gzipped extract sets
+ * agree on lengths and min/max filtering; on-disk length for everything
else.
+ */
+ public static long extractLength(Path extractFile) throws IOException {
+ long onDisk = Files.size(extractFile);
+ String name =
extractFile.getFileName().toString().toLowerCase(Locale.ROOT);
+ if (onDisk < 18 || !(name.endsWith(".gz") || name.endsWith(".gzip"))) {
+ return onDisk;
+ }
+ ByteBuffer trailer =
ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN);
+ try (FileChannel ch = FileChannel.open(extractFile,
StandardOpenOption.READ)) {
+ while (trailer.hasRemaining()) {
+ if (ch.read(trailer, onDisk - 4 + trailer.position()) < 0) {
+ return onDisk;
+ }
+ }
+ }
+ long isize = trailer.getInt(0) & 0xFFFFFFFFL;
+ // smaller than even stored blocks could yield: wrapped past 4 GiB or
multi-member
+ if (isize + (onDisk >> 10) + 64 < onDisk) {
+ return onDisk;
+ }
+ return isize;
+ }
+
public List<Metadata> loadExtract(Path extractFile) throws
ExtractReaderException {
List<Metadata> metadataList = null;
@@ -112,7 +142,7 @@ public class ExtractReader {
long length = -1L;
try {
- length = Files.size(extractFile);
+ length = extractLength(extractFile);
} catch (IOException e) {
throw new
ExtractReaderException(ExtractReaderException.TYPE.IO_EXCEPTION, e);
}
diff --git
a/tika-eval/tika-eval-app/src/test/java/org/apache/tika/eval/app/ComparerGzipExtractsTest.java
b/tika-eval/tika-eval-app/src/test/java/org/apache/tika/eval/app/ComparerGzipExtractsTest.java
new file mode 100644
index 0000000000..9cb7540c36
--- /dev/null
+++
b/tika-eval/tika-eval-app/src/test/java/org/apache/tika/eval/app/ComparerGzipExtractsTest.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.eval.app;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+import java.util.zip.GZIPOutputStream;
+
+import org.apache.commons.io.FileUtils;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import org.apache.tika.eval.app.db.H2Util;
+
+/** A gzipped extract set ({@code .json.gz}, as the file-system emitter writes
it) compares like its plain twin. */
+public class ComparerGzipExtractsTest {
+
+ private static final String[] QUERIES = {
+ "select file_path || ' ' || extract_file_length_a || ' ' ||
extract_file_length_b"
+ + " from containers order by file_path",
+ "select count(1) from profiles_a",
+ "select count(1) from profiles_b",
+ "select count(1) from extract_exceptions_a",
+ "select count(1) from extract_exceptions_b",
+ "select count(1) from content_comparisons"
+ };
+
+ @TempDir
+ Path tmp;
+
+ @Test
+ public void testGzipOnEitherSide() throws Exception {
+ Path testDirs =
Paths.get(getClass().getResource("/test-dirs").toURI());
+ Path a = testDirs.resolve("extractsA");
+ Path b = testDirs.resolve("extractsB");
+ List<String> plain = compare("plain", a, b);
+ assertEquals(plain, compare("gzB", a, gzipCopy(b,
tmp.resolve("bGz"))), "gzipped B");
+ assertEquals(plain, compare("gzA", gzipCopy(a, tmp.resolve("aGz")),
b), "gzipped A");
+ }
+
+ private List<String> compare(String name, Path a, Path b) throws Exception
{
+ Path db = tmp.resolve(name);
+ ExtractComparerRunner.main(new String[]{
+ "-a", a.toString(), "-b", b.toString(), "-d",
db.toAbsolutePath().toString()
+ });
+ List<String> results = new ArrayList<>();
+ try (Connection c = new H2Util(db).getConnection(); Statement st =
c.createStatement()) {
+ for (String q : QUERIES) {
+ try (ResultSet rs = st.executeQuery(q)) {
+ while (rs.next()) {
+ results.add(q + " -> " + rs.getString(1));
+ }
+ }
+ }
+ }
+ return results;
+ }
+
+ private static Path gzipCopy(Path src, Path dest) throws IOException {
+ FileUtils.copyDirectory(src.toFile(), dest.toFile());
+ try (Stream<Path> files = Files.walk(dest)) {
+ for (Path p : files.filter(f ->
f.toString().endsWith(".json")).toList()) {
+ try (OutputStream os = new GZIPOutputStream(
+ Files.newOutputStream(p.resolveSibling(p.getFileName()
+ ".gz")))) {
+ Files.copy(p, os);
+ }
+ Files.delete(p);
+ }
+ }
+ return dest;
+ }
+}
diff --git
a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitter.java
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitter.java
index c5e3621113..82099f0205 100644
---
a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitter.java
+++
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitter.java
@@ -16,18 +16,23 @@
*/
package org.apache.tika.pipes.emitter.fs;
+import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
+import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.List;
import java.util.UUID;
+import java.util.zip.GZIPOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -58,6 +63,10 @@ public class FileSystemEmitter extends AbstractStreamEmitter
{
// in-progress writes; crawlers of the output dir should ignore these
static final String TMP_SUFFIX = ".tmp";
+ static final String GZIP_SUFFIX = ".gz";
+
+ private static final int GZIP_BUFFER_SIZE = 64 * 1024;
+
private static final Logger LOG =
LoggerFactory.getLogger(FileSystemEmitter.class);
public static FileSystemEmitter build(ExtensionConfig pluginConfig) throws
TikaConfigException, IOException {
@@ -109,6 +118,9 @@ public class FileSystemEmitter extends
AbstractStreamEmitter {
if (!StringUtils.isBlank(config.fileExtension())) {
emitKey += "." + config.fileExtension();
}
+ if (config.gzip()) {
+ emitKey += GZIP_SUFFIX;
+ }
if (config.basePath() != null) {
Path basePath = Paths.get(config.basePath());
@@ -138,8 +150,7 @@ public class FileSystemEmitter extends
AbstractStreamEmitter {
}
Path tmp = tmpFor(output);
try {
- try (Writer writer = Files.newBufferedWriter(tmp,
StandardCharsets.UTF_8,
- StandardOpenOption.CREATE_NEW)) {
+ try (Writer writer = newWriter(tmp, config.gzip(),
StandardOpenOption.CREATE_NEW)) {
JsonMetadataList.toJson(metadataList, writer,
config.prettyPrint());
}
publish(tmp, output, config.onExists());
@@ -152,19 +163,47 @@ public class FileSystemEmitter extends
AbstractStreamEmitter {
private static void writeInPlace(List<Metadata> metadataList, Path output,
FileSystemEmitterConfig config) throws
IOException {
if (config.onExists() == FileSystemEmitterConfig.ON_EXISTS.EXCEPTION) {
- try (Writer writer = Files.newBufferedWriter(output,
StandardCharsets.UTF_8,
- StandardOpenOption.CREATE_NEW)) {
+ try (Writer writer = newWriter(output, config.gzip(),
StandardOpenOption.CREATE_NEW)) {
JsonMetadataList.toJson(metadataList, writer,
config.prettyPrint());
} catch (FileAlreadyExistsException e) {
throw alreadyExistsException(output);
}
} else {
- try (Writer writer = Files.newBufferedWriter(output,
StandardCharsets.UTF_8)) {
+ try (Writer writer = newWriter(output, config.gzip())) {
JsonMetadataList.toJson(metadataList, writer,
config.prettyPrint());
}
}
}
+ private static Writer newWriter(Path path, boolean gzip, OpenOption...
options)
+ throws IOException {
+ return new BufferedWriter(new OutputStreamWriter(newOutputStream(path,
gzip, options),
+ StandardCharsets.UTF_8));
+ }
+
+ private static OutputStream newOutputStream(Path path, boolean gzip,
OpenOption... options)
+ throws IOException {
+ OutputStream os = Files.newOutputStream(path, options);
+ return gzip ? gzip(os) : os;
+ }
+
+ static OutputStream gzip(OutputStream os) throws IOException {
+ try {
+ return new GZIPOutputStream(os, GZIP_BUFFER_SIZE) {
+ // JDK 17's close() skips closing os when finish() throws
(e.g. disk full)
+ @Override
+ public void close() throws IOException {
+ try (os) {
+ super.close();
+ }
+ }
+ };
+ } catch (IOException e) {
+ os.close();
+ throw e;
+ }
+ }
+
private static Path tmpFor(Path output) {
// sibling so the rename stays on one filesystem (and therefore atomic)
return output.resolveSibling(output.getFileName() + "." +
UUID.randomUUID() + TMP_SUFFIX);
@@ -209,9 +248,16 @@ public class FileSystemEmitter extends
AbstractStreamEmitter {
// CONTENT_ONLY emit keys are the input's own name, so without the
extension the output
// collides with the input. UNPACK keys already carry the embedded
file's own name and
// extension, so appending there would rename every unpacked file.
- if (parseContext.get(ParseMode.class) == ParseMode.CONTENT_ONLY
- && !StringUtils.isBlank(config.fileExtension())) {
- emitKey += "." + config.fileExtension();
+ // compression follows the same rule: never gzip an unpacked file
under its own name
+ boolean gzip = false;
+ if (parseContext.get(ParseMode.class) == ParseMode.CONTENT_ONLY) {
+ if (!StringUtils.isBlank(config.fileExtension())) {
+ emitKey += "." + config.fileExtension();
+ }
+ if (config.gzip()) {
+ emitKey += GZIP_SUFFIX;
+ gzip = true;
+ }
}
if (config.basePath() != null) {
@@ -232,12 +278,18 @@ public class FileSystemEmitter extends
AbstractStreamEmitter {
return;
}
if (!config.atomicWrites()) {
- copyInPlace(inputStream, output, config.onExists());
+ copyInPlace(inputStream, output, config.onExists(), gzip);
return;
}
Path tmp = tmpFor(output);
try {
- Files.copy(inputStream, tmp);
+ if (gzip) {
+ try (OutputStream os = newOutputStream(tmp, true,
StandardOpenOption.CREATE_NEW)) {
+ inputStream.transferTo(os);
+ }
+ } else {
+ Files.copy(inputStream, tmp);
+ }
publish(tmp, output, config.onExists());
} finally {
Files.deleteIfExists(tmp);
@@ -245,7 +297,20 @@ public class FileSystemEmitter extends
AbstractStreamEmitter {
}
private static void copyInPlace(InputStream inputStream, Path output,
- FileSystemEmitterConfig.ON_EXISTS
onExists) throws IOException {
+ FileSystemEmitterConfig.ON_EXISTS
onExists, boolean gzip)
+ throws IOException {
+ if (gzip) {
+ OpenOption[] options = onExists ==
FileSystemEmitterConfig.ON_EXISTS.REPLACE
+ ? new OpenOption[0] : new
OpenOption[]{StandardOpenOption.CREATE_NEW};
+ try (OutputStream os = newOutputStream(output, true, options)) {
+ inputStream.transferTo(os);
+ } catch (FileAlreadyExistsException e) {
+ if (onExists == FileSystemEmitterConfig.ON_EXISTS.EXCEPTION) {
+ throw alreadyExistsException(output);
+ }
+ }
+ return;
+ }
if (onExists == FileSystemEmitterConfig.ON_EXISTS.REPLACE) {
Files.copy(inputStream, output,
StandardCopyOption.REPLACE_EXISTING);
return;
@@ -275,7 +340,8 @@ public class FileSystemEmitter extends
AbstractStreamEmitter {
if (parseContext.hasJsonConfig(configKey)) {
JsonConfig configJson = parseContext.getJsonConfig(configKey);
if (configJson != null) {
- // basePath, allowAbsolutePaths and atomicWrites are not
runtime keys; naming one fails
+ // basePath, allowAbsolutePaths, atomicWrites and compression
are not runtime keys;
+ // naming one fails
FileSystemEmitterRuntimeConfig runtimeConfig =
PluginJson.read(configJson,
FileSystemEmitterRuntimeConfig.class);
@@ -283,7 +349,7 @@ public class FileSystemEmitter extends
AbstractStreamEmitter {
// init-time allowAbsolutePaths -- neither may be changed at
runtime.
config = new
FileSystemEmitterConfig(fileSystemEmitterConfig.basePath(),
runtimeConfig.getFileExtension(), runtimeConfig.getOnExists(),
runtimeConfig.isPrettyPrint(),
fileSystemEmitterConfig.allowAbsolutePaths(),
- fileSystemEmitterConfig.atomicWrites());
+ fileSystemEmitterConfig.atomicWrites(),
fileSystemEmitterConfig.compression());
checkConfig(config);
}
}
diff --git
a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterConfig.java
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterConfig.java
index 5365c0e62c..26a62a2b92 100644
---
a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterConfig.java
+++
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/main/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterConfig.java
@@ -20,13 +20,20 @@ import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.plugins.PluginJson;
public record FileSystemEmitterConfig(String basePath, String fileExtension,
ON_EXISTS onExists, boolean prettyPrint, boolean allowAbsolutePaths,
- Boolean atomicWrites) {
+ Boolean atomicWrites, COMPRESSION compression) {
enum ON_EXISTS {
SKIP, EXCEPTION, REPLACE
}
- /** onExists absent means EXCEPTION, atomicWrites absent means true -- the
documented defaults. */
+ enum COMPRESSION {
+ NONE, GZIP
+ }
+
+ /**
+ * onExists absent means EXCEPTION, atomicWrites absent means true,
compression absent means
+ * NONE -- the documented defaults.
+ */
public FileSystemEmitterConfig {
if (onExists == null) {
onExists = ON_EXISTS.EXCEPTION;
@@ -34,6 +41,13 @@ public record FileSystemEmitterConfig(String basePath,
String fileExtension, ON_
if (atomicWrites == null) {
atomicWrites = Boolean.TRUE;
}
+ if (compression == null) {
+ compression = COMPRESSION.NONE;
+ }
+ }
+
+ boolean gzip() {
+ return compression == COMPRESSION.GZIP;
}
public static FileSystemEmitterConfig load(final String json)
diff --git
a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/test/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterTest.java
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/test/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterTest.java
index 21500b40ce..668dbd8dd1 100644
---
a/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/test/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterTest.java
+++
b/tika-pipes/tika-pipes-plugins/tika-pipes-file-system/src/test/java/org/apache/tika/pipes/emitter/fs/FileSystemEmitterTest.java
@@ -23,11 +23,16 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream;
+import java.util.zip.GZIPInputStream;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
@@ -37,9 +42,11 @@ import org.junit.jupiter.api.io.TempDir;
import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.ParseContext;
+import org.apache.tika.pipes.api.ParseMode;
import org.apache.tika.pipes.api.emitter.Emitter;
import org.apache.tika.pipes.api.emitter.StreamEmitter;
import org.apache.tika.plugins.ExtensionConfig;
+import org.apache.tika.serialization.JsonMetadataList;
public class FileSystemEmitterTest {
@@ -212,4 +219,87 @@ public class FileSystemEmitterTest {
assertTrue(Files.isRegularFile(basePath.resolve("b.json")));
assertEquals(0, tmpFiles(basePath));
}
+
+ private StreamEmitter createGzipEmitter(Path basePath, String onExists,
boolean atomicWrites)
+ throws TikaConfigException, IOException {
+ ObjectNode config = MAPPER.createObjectNode();
+ config.put("basePath", basePath.toAbsolutePath().toString());
+ config.put("fileExtension", "json");
+ config.put("onExists", onExists);
+ config.put("atomicWrites", atomicWrites);
+ config.put("compression", "GZIP");
+ ExtensionConfig pluginConfig = new ExtensionConfig("test", "test",
config.toString());
+ return (StreamEmitter) new
FileSystemEmitterFactory().buildExtension(pluginConfig);
+ }
+
+ private static String gunzip(Path p) throws IOException {
+ try (InputStream is = new GZIPInputStream(Files.newInputStream(p))) {
+ return new String(is.readAllBytes(), StandardCharsets.UTF_8);
+ }
+ }
+
+ @Test
+ public void testGzipMetadataList() throws Exception {
+ for (boolean atomic : new boolean[]{true, false}) {
+ Path basePath = tempDir.resolve("gz-" + atomic);
+ StreamEmitter emitter = createGzipEmitter(basePath, "EXCEPTION",
atomic);
+ Metadata m = new Metadata();
+ m.set("k", "v");
+ emitter.emit("dir/a.pdf", List.of(m), new ParseContext());
+ Path out = basePath.resolve("dir/a.pdf.json.gz");
+ assertTrue(Files.isRegularFile(out), "atomicWrites=" + atomic);
+ assertFalse(Files.exists(basePath.resolve("dir/a.pdf.json")));
+ try (Reader r = new InputStreamReader(new
GZIPInputStream(Files.newInputStream(out)),
+ StandardCharsets.UTF_8)) {
+ assertEquals("v",
JsonMetadataList.fromJson(r).get(0).get("k"));
+ }
+ // onExists checks the name actually written, .gz included
+ assertThrows(IOException.class,
+ () -> emitter.emit("dir/a.pdf", List.of(m), new
ParseContext()));
+ assertEquals(0, tmpFiles(basePath.resolve("dir")));
+ }
+ }
+
+ @Test
+ public void testGzipContentOnlyButNotUnpack() throws Exception {
+ for (boolean atomic : new boolean[]{true, false}) {
+ Path basePath = tempDir.resolve("gz-stream-" + atomic);
+ StreamEmitter emitter = createGzipEmitter(basePath, "REPLACE",
atomic);
+ ParseContext contentOnly = new ParseContext();
+ contentOnly.set(ParseMode.class, ParseMode.CONTENT_ONLY);
+ emitter.emit("a.pdf", new
ByteArrayInputStream("text".getBytes(StandardCharsets.UTF_8)),
+ new Metadata(), contentOnly);
+ assertEquals("text", gunzip(basePath.resolve("a.pdf.json.gz")),
"atomicWrites=" + atomic);
+
+ // an unpacked file keeps its own name and bytes
+ emitter.emit("unpacked/image.png", new ByteArrayInputStream(new
byte[]{1, 2, 3}),
+ new Metadata(), new ParseContext());
+ assertEquals(3,
Files.size(basePath.resolve("unpacked/image.png")));
+ assertEquals(0, tmpFiles(basePath));
+ }
+ }
+
+ @Test
+ public void testGzipClosesFileWhenFinishFails() throws Exception {
+ boolean[] closed = {false};
+ // room for the 10-byte header only, so finish() hits the full disk
+ OutputStream full = new OutputStream() {
+ int written;
+
+ @Override
+ public void write(int b) throws IOException {
+ if (++written > 10) {
+ throw new IOException("No space left on device");
+ }
+ }
+
+ @Override
+ public void close() {
+ closed[0] = true;
+ }
+ };
+ OutputStream gz = FileSystemEmitter.gzip(full);
+ assertThrows(IOException.class, gz::close);
+ assertTrue(closed[0]);
+ }
}