1 package emissary.util.roll;
2
3 import emissary.roll.Rollable;
4 import emissary.util.io.FileNameGenerator;
5
6 import jakarta.annotation.Nullable;
7 import org.slf4j.Logger;
8 import org.slf4j.LoggerFactory;
9
10 import java.io.Closeable;
11 import java.io.File;
12 import java.io.FileOutputStream;
13 import java.io.FilenameFilter;
14 import java.io.IOException;
15 import java.io.OutputStream;
16 import java.nio.file.Files;
17 import java.util.UUID;
18 import java.util.concurrent.atomic.AtomicLong;
19 import java.util.concurrent.locks.ReentrantLock;
20
21
22
23
24 public class RollableFileOutputStream extends OutputStream implements Rollable {
25 private static final Logger LOG = LoggerFactory.getLogger(RollableFileOutputStream.class);
26
27 final ReentrantLock lock = new ReentrantLock();
28
29 volatile boolean rolling;
30
31 @Nullable
32 FileOutputStream fileOutputStream;
33
34 @Nullable
35 File currentFile;
36
37 FileNameGenerator namegen;
38
39 private final File dir;
40
41 long bytesWritten;
42
43 boolean deleteZeroByteFiles = true;
44
45
46
47 private final AtomicLong seq = new AtomicLong();
48
49 public RollableFileOutputStream(FileNameGenerator namegen, File dir) throws IOException {
50 if (dir == null || !dir.exists() || !dir.isDirectory()) {
51 throw new IllegalArgumentException("Directory is invalid: " + dir);
52 }
53 this.namegen = namegen;
54 this.dir = dir;
55 handleOrphanedFiles();
56 open();
57 }
58
59 public RollableFileOutputStream(FileNameGenerator namegen) throws IOException {
60 this(namegen, new File("."));
61 }
62
63 private void handleOrphanedFiles() {
64
65 FilenameFilter filter = (directory, name) -> name.startsWith(".");
66
67
68 for (File file : this.dir.listFiles(filter)) {
69 if (file.isFile()) {
70 LOG.info("Renaming orphaned file, {}, to non-dot file.", file.getName());
71 rename(file);
72 }
73 }
74 }
75
76 private void open() throws IOException {
77 File newFile = getNewFile();
78 currentFile = newFile;
79 fileOutputStream = new FileOutputStream(newFile, true);
80 }
81
82 private File getNewFile() {
83 String newName = namegen.nextFileName();
84 String dotFile = "." + newName;
85 String seqFname = "." + seq.get() + "_" + newName;
86 if (currentFile != null && (dotFile.equals(currentFile.getName()) || seqFname.equals(currentFile.getName()))) {
87 LOG.warn("Duplicate file name returned from {}. Using internal sequencer to uniquify.", namegen.getClass());
88 dotFile = "." + seq.getAndIncrement() + "_" + newName;
89 }
90 return new File(dir, dotFile);
91 }
92
93 private void closeAndRename() throws IOException {
94 fileOutputStream.flush();
95 if (!internalClose(fileOutputStream)) {
96 LOG.error("Error closing file {}", currentFile.getAbsolutePath());
97 }
98 rename(currentFile);
99 bytesWritten = 0L;
100 }
101
102 private void rename(File f) {
103 if (f.length() == 0L && deleteZeroByteFiles) {
104 try {
105 LOG.debug("Deleting Zero Byte File {}", f.getAbsolutePath());
106 Files.delete(f.toPath());
107 } catch (IOException e) {
108 LOG.error("Failed to delete zero byte file {}", f.getAbsolutePath(), e);
109 }
110 return;
111 }
112
113 String nonDot = f.getName().substring(1);
114 File nd = new File(dir, nonDot);
115
116 if (nd.exists()) {
117 LOG.error("Non dot file {} already exists. Forcing unique name.", nd.getAbsolutePath());
118 nd = new File(dir, nonDot + UUID.randomUUID());
119 }
120 if (!f.renameTo(nd)) {
121 LOG.error("Rename from {} to {} failed.", f.getAbsolutePath(), nd.getAbsolutePath());
122 }
123 }
124
125
126
127
128
129 @Override
130 public void roll() {
131 lock.lock();
132 try {
133 rolling = true;
134 closeAndRename();
135
136 open();
137 } catch (IOException e) {
138 LOG.error("Exception during roll of " + currentFile, e);
139 } finally {
140 rolling = false;
141 lock.unlock();
142 }
143 }
144
145
146
147
148
149
150 @Override
151 public boolean isRolling() {
152 return rolling;
153 }
154
155 private static boolean internalClose(@Nullable Closeable c) {
156 try {
157 if (c != null) {
158 c.close();
159 }
160 } catch (Exception e) {
161 LOG.warn("Error occurred while closing file", e);
162 return false;
163 }
164 return true;
165 }
166
167
168
169
170
171 @Override
172 public void close() throws IOException {
173 lock.lock();
174 try {
175 closeAndRename();
176 fileOutputStream = null;
177 currentFile = null;
178 } finally {
179 lock.unlock();
180 }
181 }
182
183
184
185
186
187
188 @Override
189 public void write(int b) throws IOException {
190 lock.lock();
191 try {
192 fileOutputStream.write(b);
193 bytesWritten++;
194 } finally {
195 lock.unlock();
196 }
197
198 }
199
200
201
202
203
204
205
206
207 @Override
208 public void write(byte[] b, int off, int len) throws IOException {
209 lock.lock();
210 try {
211 fileOutputStream.write(b, off, len);
212 bytesWritten += len;
213 } finally {
214 lock.unlock();
215 }
216 }
217
218
219
220
221
222
223 public long getBytesWritten() {
224 return bytesWritten;
225 }
226
227
228
229
230
231
232 public boolean isDeleteZeroByteFiles() {
233 return deleteZeroByteFiles;
234 }
235
236
237
238
239
240 public void setDeleteZeroByteFiles(boolean deleteZeroByteFiles) {
241 this.deleteZeroByteFiles = deleteZeroByteFiles;
242 }
243
244 }