/**
* This file is part of git-as-svn. It is subject to the license terms
* in the LICENSE file found in the top-level directory of this distribution
* and at http://www.gnu.org/licenses/gpl-2.0.html. No part of git-as-svn,
* including this file, may be copied, modified, propagated, or distributed
* except according to the terms contained in the LICENSE file.
*/
package svnserver.ext.gitlfs.storage.local;
import org.apache.commons.codec.binary.Hex;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import ru.bozaro.gitlfs.pointer.Constants;
import ru.bozaro.gitlfs.pointer.Pointer;
import svnserver.DateHelper;
import svnserver.HashHelper;
import svnserver.auth.User;
import svnserver.ext.gitlfs.config.LfsLayout;
import svnserver.ext.gitlfs.storage.LfsWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.zip.GZIPOutputStream;
/**
* Local storage writer.
*
* @author Artem V. Navrotskiy <bozaro@users.noreply.github.com>
*/
public class LfsLocalWriter extends LfsWriter {
private final LfsLayout layout;
@NotNull
private final File dataRoot;
@Nullable
private final File metaRoot;
@NotNull
private final File dataTemp;
@NotNull
private final File metaTemp;
private final boolean compress;
@Nullable
private final User user;
@Nullable
private OutputStream dataStream;
@NotNull
private final MessageDigest digestMd5;
@NotNull
private final MessageDigest digestSha;
private long size;
public LfsLocalWriter(@NotNull LfsLayout layout, @NotNull File dataRoot, @Nullable File metaRoot, boolean compress, @Nullable User user) throws IOException {
this.layout = layout;
this.dataRoot = dataRoot;
this.metaRoot = metaRoot;
this.compress = compress;
this.user = user;
final String prefix = UUID.randomUUID().toString();
dataTemp = new File(new File(dataRoot, "tmp"), prefix + ".tmp");
//noinspection ResultOfMethodCallIgnored
dataTemp.getParentFile().mkdirs();
dataTemp.deleteOnExit();
metaTemp = new File(new File(metaRoot, "tmp"), prefix + ".tmp");
digestMd5 = HashHelper.md5();
digestSha = HashHelper.sha256();
size = 0;
if (this.compress) {
dataStream = new GZIPOutputStream(new FileOutputStream(dataTemp));
} else {
dataStream = new FileOutputStream(dataTemp);
}
}
@Override
public void write(int b) throws IOException {
if (dataStream == null) {
throw new IllegalStateException();
}
dataStream.write(b);
digestMd5.update((byte) b);
digestSha.update((byte) b);
size += 1;
}
@Override
public void write(@NotNull byte[] b, int off, int len) throws IOException {
if (dataStream == null) {
throw new IllegalStateException();
}
dataStream.write(b, off, len);
digestMd5.update(b, off, len);
digestSha.update(b, off, len);
size += len;
}
@NotNull
@Override
public String finish(@Nullable String expectedOid) throws IOException {
if (dataStream == null) {
throw new IllegalStateException();
}
dataStream.close();
dataStream = null;
final byte[] sha = digestSha.digest();
final byte[] md5 = digestMd5.digest();
final String oid = LfsLocalStorage.OID_PREFIX + Hex.encodeHexString(sha);
if (expectedOid != null && !expectedOid.equals(oid)) {
throw new IOException("Invalid stream checksum: expected " + expectedOid + ", but actual " + oid);
}
// Write file data
final File dataPath = LfsLocalStorage.getPath(layout, dataRoot, oid, compress ? ".gz" : "");
if (dataPath == null) {
throw new IllegalStateException();
}
//noinspection ResultOfMethodCallIgnored
dataPath.getParentFile().mkdirs();
if (!dataTemp.renameTo(dataPath) && !dataPath.isFile()) {
throw new IOException("Can't rename file: " + dataTemp.getPath() + " -> " + dataPath.getPath());
}
//noinspection ResultOfMethodCallIgnored
dataTemp.delete();
// Write metadata
if (metaRoot != null) {
final File metaPath = LfsLocalStorage.getPath(layout, metaRoot, oid, ".meta");
if (metaPath == null) {
throw new IllegalStateException();
}
if (!metaPath.exists()) {
//noinspection ResultOfMethodCallIgnored
metaPath.getParentFile().mkdirs();
//noinspection ResultOfMethodCallIgnored
metaTemp.getParentFile().mkdirs();
try (FileOutputStream stream = new FileOutputStream(metaTemp)) {
final Map<String, String> map = new HashMap<>();
map.put(Constants.SIZE, String.valueOf(size));
map.put(Constants.OID, oid);
map.put(LfsLocalStorage.HASH_MD5, Hex.encodeHexString(md5));
map.put(LfsLocalStorage.CREATE_TIME, DateHelper.toISO8601(Instant.now()));
if ((user != null) && (!user.isAnonymous())) {
if (user.getEmail() != null) {
map.put(LfsLocalStorage.META_EMAIL, user.getEmail());
}
map.put(LfsLocalStorage.META_USER_NAME, user.getUserName());
map.put(LfsLocalStorage.META_REAL_NAME, user.getRealName());
}
stream.write(Pointer.serializePointer(map));
stream.close();
if (!metaTemp.renameTo(metaPath) && !metaPath.isFile()) {
throw new IOException("Can't rename file: " + metaTemp.getPath() + " -> " + metaPath.getPath());
}
} finally {
//noinspection ResultOfMethodCallIgnored
metaTemp.delete();
}
}
}
return oid;
}
@Override
public void close() throws IOException {
if (dataStream != null) {
dataStream.close();
dataStream = null;
//noinspection ResultOfMethodCallIgnored
dataTemp.delete();
}
}
}