All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.milton.zsync.ZSyncResourceFactory Maven / Gradle / Ivy

There is a newer version: 4.0.5.2400
Show newest version
/*
 * 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