package org.eclipse.aether.connector.basic;

/*
 * 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.
 */

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.Channel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;

A partially downloaded file with optional support for resume. If resume is enabled, a well-known location is used for the partial file in combination with a lock file to prevent concurrent requests from corrupting it (and wasting network bandwith). Otherwise, a (non-locked) unique temporary file is used.
/** * A partially downloaded file with optional support for resume. If resume is enabled, a well-known location is used for * the partial file in combination with a lock file to prevent concurrent requests from corrupting it (and wasting * network bandwith). Otherwise, a (non-locked) unique temporary file is used. */
final class PartialFile implements Closeable { static final String EXT_PART = ".part"; static final String EXT_LOCK = ".lock"; interface RemoteAccessChecker { void checkRemoteAccess() throws Exception; } static class LockFile { private final File lockFile; private final FileLock lock; private final AtomicBoolean concurrent; LockFile( File partFile, int requestTimeout, RemoteAccessChecker checker ) throws Exception { lockFile = new File( partFile.getPath() + EXT_LOCK ); concurrent = new AtomicBoolean( false ); lock = lock( lockFile, partFile, requestTimeout, checker, concurrent ); } private static FileLock lock( File lockFile, File partFile, int requestTimeout, RemoteAccessChecker checker, AtomicBoolean concurrent ) throws Exception { boolean interrupted = false; try { for ( long lastLength = -1L, lastTime = 0L;; ) { FileLock lock = tryLock( lockFile ); if ( lock != null ) { return lock; } long currentLength = partFile.length(); long currentTime = System.currentTimeMillis(); if ( currentLength != lastLength ) { if ( lastLength < 0L ) { concurrent.set( true ); /* * NOTE: We're going with the optimistic assumption that the other thread is downloading the * file from an equivalent repository. As a bare minimum, ensure the repository we are given * at least knows about the file and is accessible to us. */ checker.checkRemoteAccess(); LOGGER.debug( "Concurrent download of {} in progress, awaiting completion", partFile ); } lastLength = currentLength; lastTime = currentTime; } else if ( requestTimeout > 0 && currentTime - lastTime > Math.max( requestTimeout, 3 * 1000 ) ) { throw new IOException( "Timeout while waiting for concurrent download of " + partFile + " to progress" ); } try { Thread.sleep( 100 ); } catch ( InterruptedException e ) { interrupted = true; } } } finally { if ( interrupted ) { Thread.currentThread().interrupt(); } } } private static FileLock tryLock( File lockFile ) throws IOException { RandomAccessFile raf = null; FileLock lock = null; try { raf = new RandomAccessFile( lockFile, "rw" ); lock = raf.getChannel().tryLock( 0, 1, false ); if ( lock == null ) { raf.close(); raf = null; } } catch ( OverlappingFileLockException e ) { close( raf ); raf = null; lock = null; } catch ( RuntimeException | IOException e ) { close( raf ); raf = null; if ( !lockFile.delete() ) { lockFile.deleteOnExit(); } throw e; } finally { try { if ( lock == null && raf != null ) { raf.close(); } } catch ( final IOException e ) { // Suppressed due to an exception already thrown in the try block. } } return lock; } private static void close( Closeable file ) { try { if ( file != null ) { file.close(); } } catch ( IOException e ) { // Suppressed. } } public boolean isConcurrent() { return concurrent.get(); } public void close() throws IOException { Channel channel = null; try { channel = lock.channel(); lock.release(); channel.close(); channel = null; } finally { try { if ( channel != null ) { channel.close(); } } catch ( final IOException e ) { // Suppressed due to an exception already thrown in the try block. } finally { if ( !lockFile.delete() ) { lockFile.deleteOnExit(); } } } } @Override public String toString() { return lockFile + " - " + lock.isValid(); } } static class Factory { private final boolean resume; private final long resumeThreshold; private final int requestTimeout; private static final Logger LOGGER = LoggerFactory.getLogger( Factory.class ); Factory( boolean resume, long resumeThreshold, int requestTimeout ) { this.resume = resume; this.resumeThreshold = resumeThreshold; this.requestTimeout = requestTimeout; } public PartialFile newInstance( File dstFile, RemoteAccessChecker checker ) throws Exception { if ( resume ) { File partFile = new File( dstFile.getPath() + EXT_PART ); long reqTimestamp = System.currentTimeMillis(); LockFile lockFile = new LockFile( partFile, requestTimeout, checker ); if ( lockFile.isConcurrent() && dstFile.lastModified() >= reqTimestamp - 100L ) { lockFile.close(); return null; } try { if ( !partFile.createNewFile() && !partFile.isFile() ) { throw new IOException( partFile.exists() ? "Path exists but is not a file" : "Unknown error" ); } return new PartialFile( partFile, lockFile, resumeThreshold ); } catch ( IOException e ) { lockFile.close(); LOGGER.debug( "Cannot create resumable file {}: {}", partFile.getAbsolutePath(), e.getMessage(), e ); // fall through and try non-resumable/temporary file location } } File tempFile = File.createTempFile( dstFile.getName() + '-' + UUID.randomUUID().toString().replace( "-", "" ), ".tmp", dstFile.getParentFile() ); return new PartialFile( tempFile ); } } private final File partFile; private final LockFile lockFile; private final long threshold; private static final Logger LOGGER = LoggerFactory.getLogger( PartialFile.class ); private PartialFile( File partFile ) { this( partFile, null, 0L ); } private PartialFile( File partFile, LockFile lockFile, long threshold ) { this.partFile = partFile; this.lockFile = lockFile; this.threshold = threshold; } public File getFile() { return partFile; } public boolean isResume() { return lockFile != null && partFile.length() >= threshold; } public void close() throws IOException { if ( partFile.exists() && !isResume() ) { if ( !partFile.delete() && partFile.exists() ) { LOGGER.debug( "Could not delete temporary file {}", partFile ); } } if ( lockFile != null ) { lockFile.close(); } } @Override public String toString() { return String.valueOf( getFile() ); } }