io.milton.zsync.ZSyncResourceFactory Maven / Gradle / Ivy
/*
* 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 io.milton.zsync;
import io.milton.common.BufferingOutputStream;
import io.milton.common.Path;
import io.milton.http.Request.Method;
import io.milton.http.*;
import io.milton.http.exceptions.BadRequestException;
import io.milton.http.exceptions.ConflictException;
import io.milton.http.exceptions.NotAuthorizedException;
import io.milton.http.http11.auth.DigestResponse;
import io.milton.common.LogUtils;
import io.milton.common.StreamUtils;
import io.milton.resource.*;
import java.io.*;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This resource factory allows resouces to be retrieved and updated using the
* zsync protocol.
*
* Client side process for updating a local file from a server file a) assume
* the remote file is at path /somefile b) retrieve zsync metadata (ie headers
* and checksums) GET /somefile/.zsync c) implement rolling checksums and
* retrieve ranges of real file as needed with partial GETs GET /somefile
* Ranges: x-y, n-m, etc d) merge the partial ranges
*
*
* Client side process for updating a server file with a local file a) assume
* the remote file is at path /somefile b) retrieve zsync metadata (ie headers
* and checksums) GET /somefile/.zsync c) Calculate instructions and range data
* to send to server, based on the retrieved checksums d) send to server
*
*
* ....
*/
public class ZSyncResourceFactory implements ResourceFactory {
private static final Logger log = LoggerFactory.getLogger(ZSyncResourceFactory.class);
private String suffix = ".zsync";
private final ResourceFactory wrapped;
private final MetaFileMaker metaFileMaker;
private final int defaultBlockSize = 512;
private final int maxMemorySize = 100000;
public ZSyncResourceFactory(ResourceFactory wrapped) {
this.wrapped = wrapped;
metaFileMaker = new MetaFileMaker();
}
@Override
public Resource getResource(String host, String path) throws NotAuthorizedException, BadRequestException {
if (path.endsWith("/" + suffix)) {
Path p = Path.path(path);
String realPath = p.getParent().toString();
Resource r = wrapped.getResource(host, realPath);
if (r == null) {
return new ZSyncAdapterResource(null, realPath, host); // will throw bad request
} else {
if (r instanceof GetableResource) {
LogUtils.trace(log, "Found existing compatible resource at", realPath);
return new ZSyncAdapterResource((GetableResource) r, realPath, host);
} else {
return new ZSyncAdapterResource(null, realPath, host); // will throw bad request
}
}
} else {
return wrapped.getResource(host, path);
}
}
public String getSuffix() {
return suffix;
}
public void setSuffix(String suffix) {
this.suffix = suffix;
}
public ResourceFactory getWrapped() {
return wrapped;
}
public class ZSyncAdapterResource implements GetableResource, ReplaceableResource, DigestResource {
private final GetableResource r;
private final String realPath;
private final String host;
/**
* populated on POST, then used in sendContent
*/
private List ranges;
public ZSyncAdapterResource(GetableResource r, String realPath, String host) {
this.r = r;
this.realPath = realPath;
this.host = host;
}
@Override
public void sendContent(OutputStream out, Range range, Map params, String contentType) throws IOException, NotAuthorizedException, BadRequestException {
if (r == null) {
throw new BadRequestException(this, "No existing resource was found to map the zsync operation to");
}
if (ranges != null) {
log.info("sendContent: sending range data");
sendRangeData(out);
} else {
log.info("sendContent: sending meta data");
sendMetaData(params, contentType, out);
}
}
@Override
public void replaceContent(InputStream in, Long length) throws BadRequestException, ConflictException, NotAuthorizedException {
if (r == null) {
throw new BadRequestException(this, "No existing resource was found to map the zsync operation to");
}
log.trace("ZSync Replace Content: uploaded bytes " + length);
try {
File prevFile = File.createTempFile("milton-zsync", "prevFile");
FileOutputStream fout = new FileOutputStream(prevFile);
r.sendContent(fout, null, null, null);
StreamUtils.close(fout);
log.trace("Saved previous file to " + prevFile.getAbsolutePath());
File uploadData = File.createTempFile("milton-zsync", "uploadData");
fout = new FileOutputStream(uploadData);
StreamUtils.readTo(in, fout);
StreamUtils.close(fout);
log.trace("Saved PUT data to " + uploadData.getAbsolutePath());
File newFile = null;
InputStream fin = null;
BufferedInputStream uploadIn = null;
try {
fin = new FileInputStream(uploadData);
uploadIn = new BufferedInputStream(fin);
UploadReader um = new UploadReader(prevFile, uploadIn);
newFile = um.assemble();
log.trace("Assembled file and saved to " + newFile.getAbsolutePath());
String actChecksum = new SHA1(newFile).SHA1sum();
String expChecksum = um.getChecksum();
if (!actChecksum.equals(expChecksum)) {
throw new RuntimeException("Computed SHA1 checksum doesn't match expected checksum\n" + "\tExpected: " + expChecksum + "\n" + "\tActual: " + actChecksum + "\n in temp file: " + newFile.getAbsolutePath());
}
} finally {
StreamUtils.close(uploadIn);
StreamUtils.close(fin);
}
updateResourceContentActual(newFile);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
private void sendMetaData(Map params, String contentType, OutputStream out) throws RuntimeException {
Long fileLength = r.getContentLength();
int blocksize = defaultBlockSize;
if (fileLength != null) {
blocksize = metaFileMaker.computeBlockSize(fileLength);
}
MetaFileMaker.MetaData metaData;
if (r instanceof ZSyncResource) {
ZSyncResource zr = (ZSyncResource) r;
metaData = zr.getZSyncMetaData();
} else {
BufferingOutputStream bufOut = new BufferingOutputStream(maxMemorySize);
try {
r.sendContent(bufOut, null, params, contentType);
bufOut.flush();
} catch (Exception ex) {
bufOut.deleteTempFileIfExists();
throw new RuntimeException(ex);
} finally {
StreamUtils.close(bufOut);
}
InputStream in = bufOut.getInputStream();
try {
metaData = metaFileMaker.make(realPath, blocksize, fileLength == null ? 0 : fileLength, r.getModifiedDate(), in);
} finally {
StreamUtils.close(in);
}
}
metaFileMaker.write(metaData, out);
}
private void updateResourceContentActual(File mergedFile) throws BadRequestException, ConflictException, NotAuthorizedException, IOException {
if (r instanceof ReplaceableResource) {
log.trace("updateResourceContentActual: " + mergedFile.getAbsolutePath() + ", resource is replaceable");
FileInputStream fin = null;
try {
fin = new FileInputStream(mergedFile);
ReplaceableResource rr = (ReplaceableResource) r;
rr.replaceContent(fin, mergedFile.length());
} finally {
StreamUtils.close(fin);
}
} else {
log.trace("updateResourceContentActual: " + mergedFile.getAbsolutePath() + ", resource is NOT replaceable, try to replace through parent");
String parentPath = Path.path(realPath).getParent().toString();
Resource rParent = wrapped.getResource(host, parentPath);
if (rParent == null) {
throw new RuntimeException("Failed to locate parent resource to update contents. parent: " + parentPath + " host: " + host);
}
if (rParent instanceof PutableResource) {
log.trace("found parent resource, implements PutableResource");
FileInputStream fin = null;
try {
fin = new FileInputStream(mergedFile);
PutableResource putable = (PutableResource) rParent;
putable.createNew(r.getName(), fin, mergedFile.length(), r.getContentType(null));
} finally {
StreamUtils.close(fin);
}
} else {
throw new RuntimeException("Tried to update non-replaceable resource by doing createNew on parent, but the parent doesnt implement PutableResource. parent path: " + parentPath + " host: " + host + " parent type: " + rParent.getClass());
}
}
}
@Override
public Long getMaxAgeSeconds(Auth auth) {
return null;
}
@Override
public String getContentType(String accepts) {
return "application/zsyncM";
}
@Override
public Long getContentLength() {
return null;
}
@Override
public String getUniqueId() {
return null;
}
@Override
public String getName() {
return suffix;
}
@Override
public Object authenticate(String user, String password) {
if (r == null) {
return "ok"; // will fail with 400 anyway
}
return r.authenticate(user, password);
}
@Override
public boolean authorise(Request request, Method method, Auth auth) {
if (r == null) {
return true; // will fail anyway
}
return r.authorise(request, method, auth);
}
@Override
public String getRealm() {
if (r == null) {
return "Realm";
}
return r.getRealm();
}
@Override
public Date getModifiedDate() {
if (r == null) {
return null;
}
return r.getModifiedDate();
}
@Override
public String checkRedirect(Request request) {
return null;
}
@Override
public Object authenticate(DigestResponse digestRequest) {
return ((DigestResource) r).authenticate(digestRequest);
}
@Override
public boolean isDigestAllowed() {
return (r instanceof DigestResource) && ((DigestResource) r).isDigestAllowed();
}
private void sendRangeData(OutputStream out) {
PrintWriter pw = new PrintWriter(out);
for (Range range : ranges) {
pw.println(range.getRange());
}
pw.flush();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy