Skip to content

Commit fea72fe

Browse files
authored
Merge pull request #15199 from apache/15186-zip-date
Forge: Be explicit with zip directories and entry metadata
2 parents 8fd8d0c + b9574af commit fea72fe

File tree

3 files changed

+160
-20
lines changed

3 files changed

+160
-20
lines changed

grails-forge/grails-forge-core/src/main/java/org/grails/forge/io/FileSystemOutputHandler.java

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,26 @@
1818
*/
1919
package org.grails.forge.io;
2020

21-
import org.grails.forge.application.Project;
22-
import org.grails.forge.template.Template;
23-
import org.grails.forge.template.Writable;
24-
2521
import java.io.File;
2622
import java.io.IOException;
2723
import java.io.OutputStream;
2824
import java.nio.file.Files;
25+
import java.nio.file.Path;
26+
import java.nio.file.attribute.FileTime;
27+
import java.time.Instant;
28+
import java.util.ArrayList;
29+
import java.util.List;
30+
import java.util.Objects;
31+
32+
import org.grails.forge.application.Project;
33+
import org.grails.forge.template.Template;
34+
import org.grails.forge.template.Writable;
2935

3036
public class FileSystemOutputHandler implements OutputHandler {
3137

3238
File applicationDirectory;
3339
private final ConsoleOutput console;
40+
private final Instant lastModified;
3441

3542
public FileSystemOutputHandler(Project project, boolean inplace, ConsoleOutput console) throws IOException {
3643
this.console = console;
@@ -43,11 +50,13 @@ public FileSystemOutputHandler(Project project, boolean inplace, ConsoleOutput c
4350
if (applicationDirectory.exists() && !inplace) {
4451
throw new IllegalArgumentException("Cannot create the project because the target directory already exists");
4552
}
53+
lastModified = OutputUtils.createLastModified(null);
4654
}
4755

4856
public FileSystemOutputHandler(File directory, ConsoleOutput console) throws IOException {
4957
this.console = console;
5058
this.applicationDirectory = directory;
59+
lastModified = OutputUtils.createLastModified(null);
5160
}
5261

5362
/**
@@ -56,14 +65,8 @@ public FileSystemOutputHandler(File directory, ConsoleOutput console) throws IOE
5665
* @throws IOException If it cannot be resolved
5766
*/
5867
public static File getDefaultBaseDirectory() throws IOException {
59-
File baseDirectory;
6068
String userDir = System.getProperty("user.dir");
61-
if (userDir != null) {
62-
baseDirectory = new File(userDir).getCanonicalFile();
63-
} else {
64-
baseDirectory = new File("").getCanonicalFile();
65-
}
66-
return baseDirectory;
69+
return new File(Objects.requireNonNullElse(userDir, "")).getCanonicalFile();
6770
}
6871

6972
@Override
@@ -91,13 +94,51 @@ public File write(String path, Writable contents) throws IOException {
9194
if ('/' != File.separatorChar) {
9295
path = path.replace('/', File.separatorChar);
9396
}
97+
9498
File targetFile = new File(applicationDirectory, path);
95-
targetFile.getParentFile().mkdirs();
96-
targetFile.createNewFile();
99+
Path base = applicationDirectory.toPath().toAbsolutePath().normalize();
100+
Path parent = targetFile.getParentFile().toPath().toAbsolutePath().normalize();
97101

102+
// 1) Determine which parent directories don't exist yet
103+
List<Path> createdDirs = new ArrayList<>();
104+
if (!parent.startsWith(base)) {
105+
throw new IOException("Refusing to write outside base directory: " + parent);
106+
}
107+
Path p = base;
108+
for (Path seg : base.relativize(parent)) {
109+
p = p.resolve(seg);
110+
if (Files.notExists(p)) {
111+
createdDirs.add(p);
112+
}
113+
}
114+
115+
// 2) Create the needed directories
116+
Files.createDirectories(parent);
117+
118+
// 3) Write the file
119+
Files.deleteIfExists(targetFile.toPath());
98120
try (OutputStream os = Files.newOutputStream(targetFile.toPath())) {
99121
contents.write(os);
100122
}
123+
124+
// Should we set a specific mtime (SOURCE_DATE_EPOCH)
125+
if (lastModified != null) {
126+
// 4) Set the file mtime
127+
FileTime mtime = FileTime.from(lastModified);
128+
Files.setLastModifiedTime(targetFile.toPath(), mtime);
129+
130+
// 5) Set the mtime on only the directories we created
131+
// Do this after writing the file, since step 3 bumps the parent dir's mtime.
132+
for (int i = createdDirs.size() - 1; i >= 0; i--) {
133+
try {
134+
Files.setLastModifiedTime(createdDirs.get(i), mtime);
135+
} catch (IOException ignore) {
136+
// Non-fatal: some file systems may restrict touching dir times
137+
console.warning("Could not set mtime for dir: " + createdDirs.get(i));
138+
}
139+
}
140+
}
141+
101142
return targetFile;
102143
}
103144

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.grails.forge.io;
20+
21+
import java.time.Instant;
22+
23+
import io.micronaut.core.annotation.Nullable;
24+
import org.slf4j.Logger;
25+
import org.slf4j.LoggerFactory;
26+
27+
public final class OutputUtils {
28+
29+
private static final Logger LOG = LoggerFactory.getLogger(OutputUtils.class);
30+
private static final String ENV_SOURCE_DATE_EPOCH = "SOURCE_DATE_EPOCH";
31+
32+
private OutputUtils() {
33+
}
34+
35+
public static Instant createLastModified() {
36+
return createLastModified(Instant.now());
37+
}
38+
39+
@Nullable
40+
public static Instant createLastModified(@Nullable Instant fallback) {
41+
String raw = System.getenv(ENV_SOURCE_DATE_EPOCH);
42+
if (raw == null) {
43+
// reproducible timestamp not requested
44+
return fallback;
45+
}
46+
47+
String trimmed = raw.trim();
48+
if (trimmed.isEmpty()) {
49+
LOG.warn("Environment variable {} is set but empty; using fallback.", ENV_SOURCE_DATE_EPOCH);
50+
return fallback;
51+
}
52+
53+
try {
54+
long epochSeconds = Long.parseLong(trimmed);
55+
return Instant.ofEpochSecond(epochSeconds);
56+
} catch (Exception ex) {
57+
LOG.warn("Could not interpret {}=[{}]; using fallback. Reason: {}", ENV_SOURCE_DATE_EPOCH, raw, ex.toString());
58+
return fallback;
59+
}
60+
}
61+
}

grails-forge/grails-forge-core/src/main/java/org/grails/forge/io/ZipOutputHandler.java

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,17 @@
3131
import java.nio.file.Files;
3232
import java.nio.file.Path;
3333
import java.nio.file.Paths;
34+
import java.nio.file.attribute.FileTime;
35+
import java.util.HashSet;
36+
import java.util.Set;
3437

3538
public class ZipOutputHandler implements OutputHandler {
3639

3740
private final ZipArchiveOutputStream zipOutputStream;
3841
private final File zip;
3942
private final String directory;
43+
private final Set<String> createdDirs = new HashSet<>();
44+
private final FileTime lastModified;
4045

4146
public ZipOutputHandler(Project project) throws IOException {
4247
File baseDirectory = new File(".").getCanonicalFile();
@@ -48,18 +53,18 @@ public ZipOutputHandler(Project project) throws IOException {
4853
zip.createNewFile();
4954
zipOutputStream = new ZipArchiveOutputStream(Files.newOutputStream(zip.toPath()));
5055
directory = project.getName();
56+
lastModified = FileTime.from(OutputUtils.createLastModified());
5157
}
5258

5359
public ZipOutputHandler(OutputStream outputStream) {
54-
zip = null;
55-
zipOutputStream = new ZipArchiveOutputStream(outputStream);
56-
directory = null;
60+
this(null, outputStream);
5761
}
5862

5963
public ZipOutputHandler(String projectName, OutputStream outputStream) {
6064
zip = null;
6165
zipOutputStream = new ZipArchiveOutputStream(outputStream);
6266
directory = projectName;
67+
lastModified = FileTime.from(OutputUtils.createLastModified());
6368
}
6469

6570
@Override
@@ -78,10 +83,18 @@ public boolean exists(String path) {
7883

7984
@Override
8085
public void write(String path, Template contents) throws IOException {
81-
ZipArchiveEntry zipEntry = new ZipArchiveEntry(directory != null ? StringUtils.prependUri(directory, path) : path);
82-
if (contents.isExecutable()) {
83-
zipEntry.setUnixMode(UnixStat.FILE_FLAG | 0755);
84-
}
86+
String entryName = (directory != null ? StringUtils.prependUri(directory, path) : path);
87+
88+
// ensure parent directories exist as explicit dir entries
89+
// https://github.com/apache/grails-core/issues/15186
90+
createParentDirs(entryName, lastModified);
91+
92+
ZipArchiveEntry zipEntry = new ZipArchiveEntry(entryName);
93+
setZipEntryMetadata(
94+
zipEntry,
95+
lastModified,
96+
UnixStat.FILE_FLAG | (contents.isExecutable() ? 0755 : 0644)
97+
);
8598
zipOutputStream.putArchiveEntry(zipEntry);
8699
contents.write(zipOutputStream);
87100
zipOutputStream.closeArchiveEntry();
@@ -92,4 +105,29 @@ public void close() throws IOException {
92105
zipOutputStream.finish();
93106
zipOutputStream.close();
94107
}
108+
109+
private void createParentDirs(String entryName, FileTime lastModified) throws IOException {
110+
int slash = entryName.lastIndexOf('/');
111+
if (slash < 0) {
112+
return;
113+
}
114+
115+
int i = 0;
116+
while ((i = entryName.indexOf('/', i)) >= 0) {
117+
String dir = entryName.substring(0, i + 1);
118+
if (createdDirs.add(dir)) {
119+
ZipArchiveEntry directoryEntry = new ZipArchiveEntry(dir);
120+
setZipEntryMetadata(directoryEntry, lastModified, UnixStat.DIR_FLAG | 0755);
121+
zipOutputStream.putArchiveEntry(directoryEntry);
122+
zipOutputStream.closeArchiveEntry();
123+
}
124+
i++;
125+
}
126+
}
127+
128+
private void setZipEntryMetadata(ZipArchiveEntry zipEntry, FileTime lastModified, int unixMode) {
129+
zipEntry.setLastModifiedTime(lastModified);
130+
zipEntry.setTime(lastModified.toMillis());
131+
zipEntry.setUnixMode(unixMode);
132+
}
95133
}

0 commit comments

Comments
 (0)