add nextflow d30e48d
This commit is contained in:
43
nextflow/modules/nf-httpfs/build.gradle
Normal file
43
nextflow/modules/nf-httpfs/build.gradle
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* Licensed 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.
|
||||
*/
|
||||
|
||||
|
||||
apply plugin: 'groovy'
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs = []
|
||||
main.groovy.srcDirs = ['src/main']
|
||||
main.resources.srcDirs = ['src/resources']
|
||||
test.groovy.srcDirs = ['src/test']
|
||||
test.java.srcDirs = []
|
||||
test.resources.srcDirs = []
|
||||
}
|
||||
|
||||
|
||||
dependencies {
|
||||
api project(':nf-commons')
|
||||
api "ch.qos.logback:logback-classic:1.5.26"
|
||||
api "org.apache.groovy:groovy:4.0.31"
|
||||
api "org.apache.groovy:groovy-nio:4.0.31"
|
||||
api("com.esotericsoftware.kryo:kryo:2.24.0") { exclude group: 'com.esotericsoftware.minlog', module: 'minlog' }
|
||||
|
||||
/* testImplementation inherited from top gradle build file */
|
||||
testImplementation "org.apache.groovy:groovy-json:4.0.31" // needed by wiremock
|
||||
testImplementation ('org.wiremock:wiremock:3.13.1') { exclude module: 'groovy-all' }
|
||||
|
||||
testImplementation(testFixtures(project(":nextflow")))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* Licensed 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 nextflow.file.http
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
|
||||
/**
|
||||
* Implements a {@link FilterInputStream} that checks the expected length of bytes have been
|
||||
* read when closing the stream or throws an error otherwise
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
@CompileStatic
|
||||
class FixedInputStream extends FilterInputStream {
|
||||
|
||||
private final long length
|
||||
private long bytesRead
|
||||
|
||||
FixedInputStream(InputStream inputStream, long len) {
|
||||
super(inputStream)
|
||||
this.length = len
|
||||
}
|
||||
|
||||
@Override
|
||||
int read() throws IOException {
|
||||
final result = super.read()
|
||||
if( result!=-1 )
|
||||
bytesRead++
|
||||
return result
|
||||
}
|
||||
|
||||
@Override
|
||||
int read(byte[] b, int off, int len) throws IOException {
|
||||
final result = super.read(b, off, len)
|
||||
if( result!=-1 )
|
||||
bytesRead += result
|
||||
return result
|
||||
}
|
||||
|
||||
@Override
|
||||
long skip(long n) throws IOException {
|
||||
long skipped = super.skip(n)
|
||||
bytesRead += skipped
|
||||
return skipped
|
||||
}
|
||||
|
||||
@Override
|
||||
int available() throws IOException {
|
||||
super.available()
|
||||
}
|
||||
|
||||
@Override
|
||||
void close() throws IOException {
|
||||
if( bytesRead != length )
|
||||
throw new IOException("Read data length does not match expected size - bytes read: ${bytesRead}; expected: ${length}")
|
||||
super.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* Licensed 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 nextflow.file.http
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
|
||||
/**
|
||||
* JSR-203 file system provider for FTP protocol
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
@CompileStatic
|
||||
class FtpFileSystemProvider extends XFileSystemProvider {
|
||||
|
||||
@Override
|
||||
String getScheme() {
|
||||
return 'ftp'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* Licensed 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 nextflow.file.http
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
|
||||
/**
|
||||
* JSR-203 file system provider for HTTP protocol
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
@CompileStatic
|
||||
class HttpFileSystemProvider extends XFileSystemProvider {
|
||||
|
||||
private static final String SCHEME = 'http'
|
||||
|
||||
@Override
|
||||
String getScheme() {
|
||||
return SCHEME
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* Licensed 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 nextflow.file.http
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
|
||||
/**
|
||||
* JSR-203 file system provider for HTTPS protocol
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
|
||||
@CompileStatic
|
||||
class HttpsFileSystemProvider extends XFileSystemProvider {
|
||||
|
||||
@Override
|
||||
String getScheme() {
|
||||
return 'https'
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* Licensed 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 nextflow.file.http
|
||||
|
||||
/**
|
||||
* Implements a pluggable authentication provider for {@link XFileSystemProvider}
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
interface XAuthProvider {
|
||||
|
||||
/**
|
||||
* Implementing class should check whenever accept and authorise the connection
|
||||
*
|
||||
* @param connection A {@link URLConnection} object instance to be authorised
|
||||
* @return {@code true} if the connection object has been authorised or {@code false} otherwise
|
||||
*/
|
||||
boolean authorize( URLConnection connection )
|
||||
|
||||
boolean refreshToken( URLConnection connection )
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* Licensed 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 nextflow.file.http
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
|
||||
/**
|
||||
* Provides a pluggable authentication registry for {@link XFileSystemProvider}
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
@Singleton(strict = false)
|
||||
@CompileStatic
|
||||
class XAuthRegistry {
|
||||
|
||||
private List<XAuthProvider> providers = new ArrayList<>()
|
||||
|
||||
protected XAuthRegistry() {}
|
||||
|
||||
void register(XAuthProvider provider) {
|
||||
providers.add(provider)
|
||||
}
|
||||
|
||||
void unregister(XAuthProvider provider) {
|
||||
final p = providers.indexOf(provider)
|
||||
if( p!=-1 )
|
||||
providers.remove(p)
|
||||
}
|
||||
|
||||
boolean authorize(URLConnection connection) {
|
||||
for( XAuthProvider it : providers ) {
|
||||
if( it.authorize(connection) )
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
boolean refreshToken(URLConnection connection) {
|
||||
for( XAuthProvider it : providers ) {
|
||||
if( it.refreshToken(connection) )
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* Licensed 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 nextflow.file.http
|
||||
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
import java.nio.file.attribute.FileTime
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
import groovy.transform.PackageScope
|
||||
|
||||
/**
|
||||
* Implements a {@link BasicFileAttributes} for http/ftp
|
||||
* file system providers
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
* @author Emilio Palumbo <emilio.palumbo@crg.eu>
|
||||
*/
|
||||
@PackageScope
|
||||
@CompileStatic
|
||||
class XFileAttributes implements BasicFileAttributes {
|
||||
|
||||
private FileTime lastModifiedTime
|
||||
private long size
|
||||
|
||||
XFileAttributes(FileTime lastModifiedTime, long size) {
|
||||
this.lastModifiedTime = lastModifiedTime
|
||||
this.size = size
|
||||
}
|
||||
|
||||
@Override
|
||||
FileTime lastModifiedTime() {
|
||||
return lastModifiedTime
|
||||
}
|
||||
|
||||
@Override
|
||||
FileTime lastAccessTime() {
|
||||
return null
|
||||
}
|
||||
|
||||
@Override
|
||||
FileTime creationTime() {
|
||||
return null
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isRegularFile() {
|
||||
return true
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isDirectory() {
|
||||
return false
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isSymbolicLink() {
|
||||
return false
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isOther() {
|
||||
return false
|
||||
}
|
||||
|
||||
@Override
|
||||
long size() {
|
||||
return size
|
||||
}
|
||||
|
||||
@Override
|
||||
Object fileKey() {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* Licensed 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 nextflow.file.http
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
|
||||
import java.nio.file.FileStore
|
||||
import java.nio.file.FileSystem
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.PathMatcher
|
||||
import java.nio.file.WatchService
|
||||
import java.nio.file.attribute.UserPrincipalLookupService
|
||||
import java.nio.file.spi.FileSystemProvider
|
||||
|
||||
import groovy.transform.PackageScope
|
||||
|
||||
/**
|
||||
* Implements a read-only JSR-203 compliant file system for http/ftp protocols
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
* @author Emilio Palumbo <emilio.palumbo@crg.eu>
|
||||
*/
|
||||
@PackageScope
|
||||
@CompileStatic
|
||||
class XFileSystem extends FileSystem {
|
||||
|
||||
static private String PATH_SEPARATOR = '/'
|
||||
|
||||
private XFileSystemProvider provider
|
||||
|
||||
private URI base
|
||||
|
||||
/*
|
||||
* Only needed to prevent serialization issues - see https://github.com/nextflow-io/nextflow/issues/5208
|
||||
*/
|
||||
protected XFileSystem(){}
|
||||
|
||||
XFileSystem(XFileSystemProvider provider, URI base) {
|
||||
this.provider = provider
|
||||
this.base = base
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean equals( Object other ) {
|
||||
if( this.class != other.class ) return false
|
||||
final that = (XFileSystem)other
|
||||
this.provider == that.provider && this.base == that.base
|
||||
}
|
||||
|
||||
@Override
|
||||
int hashCode() {
|
||||
Objects.hash(provider,base)
|
||||
}
|
||||
|
||||
@Override
|
||||
FileSystemProvider provider() {
|
||||
return provider
|
||||
}
|
||||
|
||||
URI getBaseUri() {
|
||||
return base
|
||||
}
|
||||
|
||||
@Override
|
||||
void close() throws IOException {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isOpen() {
|
||||
return true
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isReadOnly() {
|
||||
return true
|
||||
}
|
||||
|
||||
@Override
|
||||
String getSeparator() {
|
||||
return PATH_SEPARATOR
|
||||
}
|
||||
|
||||
@Override
|
||||
Iterable<Path> getRootDirectories() {
|
||||
return null
|
||||
}
|
||||
|
||||
@Override
|
||||
Iterable<FileStore> getFileStores() {
|
||||
return null
|
||||
}
|
||||
|
||||
@Override
|
||||
Set<String> supportedFileAttributeViews() {
|
||||
return null
|
||||
}
|
||||
|
||||
@Override
|
||||
Path getPath(String first, String... more) {
|
||||
return new XPath(this,first,more)
|
||||
}
|
||||
|
||||
@Override
|
||||
PathMatcher getPathMatcher(String syntaxAndPattern) {
|
||||
return null
|
||||
}
|
||||
|
||||
@Override
|
||||
UserPrincipalLookupService getUserPrincipalLookupService() {
|
||||
throw new UnsupportedOperationException('User Principal Lookup Service not supported')
|
||||
}
|
||||
|
||||
@Override
|
||||
WatchService newWatchService() throws IOException {
|
||||
throw new UnsupportedOperationException('Watch Service not supported')
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* Licensed 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 nextflow.file.http
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
import nextflow.SysEnv
|
||||
|
||||
/**
|
||||
* Hold HTTP/FTP virtual file system configuration
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
@CompileStatic
|
||||
class XFileSystemConfig {
|
||||
|
||||
static XFileSystemConfig instance = new XFileSystemConfig()
|
||||
|
||||
static final public String DEFAULT_RETRY_CODES = '404,410'
|
||||
|
||||
static final public int MAX_REDIRECT_HOPS = 5
|
||||
|
||||
static final public int DEFAULT_BACK_OFF_BASE = 3
|
||||
|
||||
static final public int DEFAULT_BACK_OFF_DELAY = 250
|
||||
|
||||
static final public int DEFAULT_MAX_ATTEMPTS = 3
|
||||
|
||||
private int maxAttempts = DEFAULT_MAX_ATTEMPTS
|
||||
|
||||
private int backOffBase = DEFAULT_BACK_OFF_BASE
|
||||
|
||||
private int backOffDelay = DEFAULT_BACK_OFF_DELAY
|
||||
|
||||
private List<Integer> retryCodes
|
||||
|
||||
{
|
||||
maxAttempts = config('NXF_HTTPFS_MAX_ATTEMPTS', DEFAULT_MAX_ATTEMPTS) as Integer
|
||||
backOffBase = config('NXF_HTTPFS_BACKOFF_BASE', DEFAULT_BACK_OFF_BASE) as Integer
|
||||
backOffDelay = config('NXF_HTTPFS_DELAY', DEFAULT_BACK_OFF_DELAY) as Integer
|
||||
retryCodes = config('NXF_HTTPFS_RETRY_CODES', DEFAULT_RETRY_CODES).tokenize(',').collect( val -> val as Integer )
|
||||
}
|
||||
|
||||
static String config(String name, def defValue) {
|
||||
return SysEnv.containsKey(name) ? SysEnv.get(name) : defValue.toString()
|
||||
}
|
||||
|
||||
int maxAttempts() { maxAttempts }
|
||||
|
||||
int backOffBase() { backOffBase }
|
||||
|
||||
int backOffDelay() { backOffDelay }
|
||||
|
||||
List<Integer> retryCodes() { retryCodes }
|
||||
|
||||
static XFileSystemConfig config() { return instance }
|
||||
}
|
||||
@@ -0,0 +1,502 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* Licensed 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 nextflow.file.http
|
||||
|
||||
import nextflow.file.CopyMoveHelper
|
||||
|
||||
import static nextflow.file.http.XFileSystemConfig.*
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.SeekableByteChannel
|
||||
import java.nio.file.AccessDeniedException
|
||||
import java.nio.file.AccessMode
|
||||
import java.nio.file.CopyOption
|
||||
import java.nio.file.DirectoryStream
|
||||
import java.nio.file.FileStore
|
||||
import java.nio.file.FileSystem
|
||||
import java.nio.file.FileSystemNotFoundException
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.LinkOption
|
||||
import java.nio.file.OpenOption
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.ProviderMismatchException
|
||||
import java.nio.file.StandardOpenOption
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
import java.nio.file.attribute.FileAttribute
|
||||
import java.nio.file.attribute.FileAttributeView
|
||||
import java.nio.file.attribute.FileTime
|
||||
import java.nio.file.spi.FileSystemProvider
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
import groovy.transform.PackageScope
|
||||
import groovy.util.logging.Slf4j
|
||||
import nextflow.SysEnv
|
||||
import nextflow.extension.FilesEx
|
||||
import nextflow.file.FileHelper
|
||||
import nextflow.util.InsensitiveMap
|
||||
import sun.net.www.protocol.ftp.FtpURLConnection
|
||||
/**
|
||||
* Implements a read-only JSR-203 compliant file system provider for http/ftp protocols
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
* @author Emilio Palumbo <emilio.palumbo@crg.eu>
|
||||
*/
|
||||
@Slf4j
|
||||
@PackageScope
|
||||
@CompileStatic
|
||||
abstract class XFileSystemProvider extends FileSystemProvider {
|
||||
|
||||
private Map<URI, FileSystem> fileSystemMap = new LinkedHashMap<>(20)
|
||||
|
||||
private static final int[] REDIRECT_CODES = [301, 302, 307, 308]
|
||||
|
||||
protected static String config(String name, def defValue) {
|
||||
return SysEnv.containsKey(name) ? SysEnv.get(name) : defValue.toString()
|
||||
}
|
||||
|
||||
static private URI key(String s, String a) {
|
||||
new URI("$s://$a")
|
||||
}
|
||||
|
||||
static private URI key(URI uri) {
|
||||
final base = uri.authority
|
||||
int p = base.indexOf('@')
|
||||
if( p==-1 )
|
||||
return key(uri.scheme.toLowerCase(), base.toLowerCase())
|
||||
else {
|
||||
final user = base.substring(0,p)
|
||||
final host = base.substring(p)
|
||||
return key(uri.scheme.toLowerCase(), user + host.toLowerCase())
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
FileSystem newFileSystem(URI uri, Map<String, ?> env) throws IOException {
|
||||
final scheme = uri.scheme.toLowerCase()
|
||||
|
||||
if( scheme != this.getScheme() )
|
||||
throw new IllegalArgumentException("Not a valid ${getScheme().toUpperCase()} scheme: $scheme")
|
||||
|
||||
final base = key(uri)
|
||||
if (fileSystemMap.containsKey(base))
|
||||
throw new IllegalStateException("File system `$base` already exists")
|
||||
|
||||
return new XFileSystem(this, base)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an existing {@code FileSystem} created by this provider.
|
||||
*
|
||||
* <p> This method returns a reference to a {@code FileSystem} that was
|
||||
* created by invoking the {@link #newFileSystem(URI,Map) newFileSystem(URI,Map)}
|
||||
* method. File systems created the {@link #newFileSystem(Path,Map)
|
||||
* newFileSystem(Path,Map)} method are not returned by this method.
|
||||
* The file system is identified by its {@code URI}. Its exact form
|
||||
* is highly provider dependent. In the case of the default provider the URI's
|
||||
* path component is {@code "/"} and the authority, query and fragment components
|
||||
* are undefined (Undefined components are represented by {@code null}).
|
||||
*
|
||||
* @param uri
|
||||
* URI reference
|
||||
*
|
||||
* @return The file system
|
||||
*
|
||||
* @throws IllegalArgumentException
|
||||
* If the pre-conditions for the {@code uri} parameter aren't met
|
||||
* @throws FileSystemNotFoundException
|
||||
* If the file system does not exist
|
||||
* @throws SecurityException
|
||||
* If a security manager is installed and it denies an unspecified
|
||||
* permission.
|
||||
*/
|
||||
@Override
|
||||
FileSystem getFileSystem(URI uri) {
|
||||
getFileSystem(uri,false)
|
||||
}
|
||||
|
||||
FileSystem getFileSystem(URI uri, boolean canCreate) {
|
||||
assert fileSystemMap != null
|
||||
|
||||
final scheme = uri.scheme.toLowerCase()
|
||||
|
||||
if( scheme != this.getScheme() )
|
||||
throw new IllegalArgumentException("Not a valid ${getScheme().toUpperCase()} scheme: $scheme")
|
||||
|
||||
final key = key(uri)
|
||||
|
||||
if( !canCreate ) {
|
||||
FileSystem result = fileSystemMap[key]
|
||||
if( result==null )
|
||||
throw new FileSystemNotFoundException("File system not found: $key")
|
||||
return result
|
||||
}
|
||||
|
||||
synchronized (fileSystemMap) {
|
||||
FileSystem result = fileSystemMap[key]
|
||||
if( result==null ) {
|
||||
result = newFileSystem(uri,Collections.emptyMap())
|
||||
fileSystemMap[key] = result
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
Path getPath(URI uri) {
|
||||
def path = uri.path
|
||||
if( !path.contains('?') && uri.query )
|
||||
path += '?' + uri.query
|
||||
return getFileSystem(uri,true).getPath(path)
|
||||
}
|
||||
|
||||
protected String auth(String userInfo) {
|
||||
final String BEARER = 'x-oauth-bearer:'
|
||||
int p = userInfo.indexOf(BEARER)
|
||||
if( p!=-1 ) {
|
||||
final token = userInfo.substring(BEARER.length())
|
||||
return "Bearer $token"
|
||||
}
|
||||
else {
|
||||
return "Basic ${userInfo.getBytes().encodeBase64()}"
|
||||
}
|
||||
}
|
||||
|
||||
protected URLConnection toConnection(Path path) {
|
||||
final url = path.toUri().toURL()
|
||||
log.trace "File remote URL: $url"
|
||||
return toConnection0(url, 0)
|
||||
}
|
||||
|
||||
protected URLConnection toConnection0(URL url, int attempt) {
|
||||
final conn = url.openConnection()
|
||||
conn.setRequestProperty("User-Agent", 'Nextflow/httpfs')
|
||||
if( conn instanceof HttpURLConnection ) {
|
||||
// by default HttpURLConnection does redirect only within the same host
|
||||
// disable the built-in to implement custom redirection logic (see below)
|
||||
conn.setInstanceFollowRedirects(false)
|
||||
}
|
||||
if( url.userInfo ) {
|
||||
conn.setRequestProperty("Authorization", auth(url.userInfo));
|
||||
}
|
||||
else {
|
||||
XAuthRegistry.instance.authorize(conn)
|
||||
}
|
||||
if ( conn instanceof HttpURLConnection && conn.getResponseCode() in REDIRECT_CODES && attempt < MAX_REDIRECT_HOPS ) {
|
||||
final header = InsensitiveMap.of(conn.getHeaderFields())
|
||||
final location = header.get("Location")?.get(0)
|
||||
log.debug "Remote redirect location=$location; attempt=$attempt"
|
||||
final newUrl = new URI(absLocation(location,url)).toURL()
|
||||
if( url.protocol=='https' && newUrl.protocol=='http' )
|
||||
throw new IOException("Refuse to follow redirection from HTTPS to HTTP (unsafe) URL - origin: $url - target: $newUrl")
|
||||
return toConnection0(newUrl, attempt+1)
|
||||
}
|
||||
else if( conn instanceof HttpURLConnection && conn.getResponseCode() in config().retryCodes() && attempt < config().maxAttempts() ) {
|
||||
final delay = (Math.pow(config().backOffBase(), attempt) as long) * config().backOffDelay()
|
||||
log.debug "Got HTTP error=${conn.getResponseCode()} waiting for ${delay}ms (attempt=${attempt+1})"
|
||||
Thread.sleep(delay)
|
||||
return toConnection0(url, attempt+1)
|
||||
}
|
||||
else if( conn instanceof HttpURLConnection && conn.getResponseCode()==401 && attempt==0 ) {
|
||||
if( XAuthRegistry.instance.refreshToken(conn) ) {
|
||||
return toConnection0(url, attempt+1)
|
||||
}
|
||||
}
|
||||
return conn
|
||||
}
|
||||
|
||||
protected String absLocation(String location, URL target) {
|
||||
assert location, "Missing location argument"
|
||||
assert target, "Missing target URL argument"
|
||||
|
||||
final base = FileHelper.baseUrl(location)
|
||||
if( base )
|
||||
return location
|
||||
if( !location.startsWith('/') )
|
||||
location = '/' + location
|
||||
return "${target.protocol}://${target.authority}$location"
|
||||
}
|
||||
|
||||
@Override
|
||||
SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
|
||||
|
||||
if (path.class != XPath)
|
||||
throw new ProviderMismatchException()
|
||||
|
||||
if (options.size() > 0) {
|
||||
for (OpenOption opt: options) {
|
||||
// All OpenOption values except for APPEND and WRITE are allowed
|
||||
if (opt == StandardOpenOption.APPEND || opt == StandardOpenOption.WRITE)
|
||||
throw new UnsupportedOperationException("'$opt' not allowed");
|
||||
}
|
||||
}
|
||||
|
||||
final conn = toConnection(path)
|
||||
final length = conn.getContentLengthLong()
|
||||
final target = length>0
|
||||
? new FixedInputStream(conn.getInputStream(),length)
|
||||
: conn.getInputStream()
|
||||
final stream = new BufferedInputStream(target)
|
||||
|
||||
new SeekableByteChannel() {
|
||||
|
||||
private long _position
|
||||
|
||||
@Override
|
||||
int read(ByteBuffer buffer) throws IOException {
|
||||
def data=0
|
||||
int len=0
|
||||
while( buffer.hasRemaining() && (data=stream.read())!=-1 ) {
|
||||
buffer.put((byte)data)
|
||||
len++
|
||||
}
|
||||
_position += len
|
||||
return len ?: -1
|
||||
}
|
||||
|
||||
@Override
|
||||
int write(ByteBuffer src) throws IOException {
|
||||
throw new UnsupportedOperationException("Write operation not supported")
|
||||
}
|
||||
|
||||
@Override
|
||||
long position() throws IOException {
|
||||
return _position
|
||||
}
|
||||
|
||||
@Override
|
||||
SeekableByteChannel position(long newPosition) throws IOException {
|
||||
throw new UnsupportedOperationException("Position operation not supported")
|
||||
}
|
||||
|
||||
@Override
|
||||
long size() throws IOException {
|
||||
// this value is going to be used as the buffer size
|
||||
// file related operation. See for example {@link Files#readAllBytes}
|
||||
return 8192
|
||||
}
|
||||
|
||||
@Override
|
||||
SeekableByteChannel truncate(long unused) throws IOException {
|
||||
throw new UnsupportedOperationException("Truncate operation not supported")
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isOpen() {
|
||||
return true
|
||||
}
|
||||
|
||||
@Override
|
||||
void close() throws IOException {
|
||||
stream.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a file, returning an input stream to read from the file. This
|
||||
* method works in exactly the manner specified by the {@link
|
||||
* Files#newInputStream} method.
|
||||
*
|
||||
* <p> The default implementation of this method opens a channel to the file
|
||||
* as if by invoking the {@link #newByteChannel} method and constructs a
|
||||
* stream that reads bytes from the channel. This method should be overridden
|
||||
* where appropriate.
|
||||
*
|
||||
* @param path
|
||||
* the path to the file to open
|
||||
* @param options
|
||||
* options specifying how the file is opened
|
||||
*
|
||||
* @return a new input stream
|
||||
*
|
||||
* @throws IllegalArgumentException
|
||||
* if an invalid combination of options is specified
|
||||
* @throws UnsupportedOperationException
|
||||
* if an unsupported option is specified
|
||||
* @throws IOException
|
||||
* if an I/O error occurs
|
||||
* @throws SecurityException
|
||||
* In the case of the default provider, and a security manager is
|
||||
* installed, the {@link SecurityManager#checkRead(String) checkRead}
|
||||
* method is invoked to check read access to the file.
|
||||
*/
|
||||
@Override
|
||||
InputStream newInputStream(Path path, OpenOption... options)
|
||||
throws IOException
|
||||
{
|
||||
if (path.class != XPath)
|
||||
throw new ProviderMismatchException()
|
||||
|
||||
if (options.length > 0) {
|
||||
for (OpenOption opt: options) {
|
||||
// All OpenOption values except for APPEND and WRITE are allowed
|
||||
if (opt == StandardOpenOption.APPEND ||
|
||||
opt == StandardOpenOption.WRITE)
|
||||
throw new UnsupportedOperationException("'$opt' not allowed");
|
||||
}
|
||||
}
|
||||
|
||||
final conn = toConnection(path)
|
||||
final length = conn.getContentLengthLong()
|
||||
// only apply the FixedInputStream check if staging files
|
||||
return length>0 && CopyMoveHelper.IN_FOREIGN_COPY.get()
|
||||
? new FixedInputStream(conn.getInputStream(), length)
|
||||
: conn.getInputStream()
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens or creates a file, returning an output stream that may be used to
|
||||
* write bytes to the file. This method works in exactly the manner
|
||||
* specified by the {@link Files#newOutputStream} method.
|
||||
*
|
||||
* <p> The default implementation of this method opens a channel to the file
|
||||
* as if by invoking the {@link #newByteChannel} method and constructs a
|
||||
* stream that writes bytes to the channel. This method should be overridden
|
||||
* where appropriate.
|
||||
*
|
||||
* @param path
|
||||
* the path to the file to open or create
|
||||
* @param options
|
||||
* options specifying how the file is opened
|
||||
*
|
||||
* @return a new output stream
|
||||
*
|
||||
* @throws IllegalArgumentException
|
||||
* if {@code options} contains an invalid combination of options
|
||||
* @throws UnsupportedOperationException
|
||||
* if an unsupported option is specified
|
||||
* @throws IOException
|
||||
* if an I/O error occurs
|
||||
* @throws SecurityException
|
||||
* In the case of the default provider, and a security manager is
|
||||
* installed, the {@link SecurityManager#checkWrite(String) checkWrite}
|
||||
* method is invoked to check write access to the file. The {@link
|
||||
* SecurityManager#checkDelete(String) checkDelete} method is
|
||||
* invoked to check delete access if the file is opened with the
|
||||
* {@code DELETE_ON_CLOSE} option.
|
||||
*/
|
||||
@Override
|
||||
public OutputStream newOutputStream(Path path, OpenOption... options)
|
||||
throws IOException
|
||||
{
|
||||
throw new UnsupportedOperationException("Write not supported by ${getScheme().toUpperCase()} file system provider")
|
||||
}
|
||||
|
||||
@Override
|
||||
DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter) throws IOException {
|
||||
throw new UnsupportedOperationException("Directory listing unsupported by ${getScheme().toUpperCase()} file system provider")
|
||||
}
|
||||
|
||||
@Override
|
||||
void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException {
|
||||
throw new UnsupportedOperationException("Create directory not supported by ${getScheme().toUpperCase()} file system provider")
|
||||
}
|
||||
|
||||
@Override
|
||||
void delete(Path path) throws IOException {
|
||||
throw new UnsupportedOperationException("Delete not supported by ${getScheme().toUpperCase()} file system provider")
|
||||
}
|
||||
|
||||
@Override
|
||||
void copy(Path source, Path target, CopyOption... options) throws IOException {
|
||||
throw new UnsupportedOperationException("Copy not supported by ${getScheme().toUpperCase()} file system provider")
|
||||
}
|
||||
|
||||
@Override
|
||||
void move(Path source, Path target, CopyOption... options) throws IOException {
|
||||
throw new UnsupportedOperationException("Move not supported by ${getScheme().toUpperCase()} file system provider")
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isSameFile(Path path, Path path2) throws IOException {
|
||||
return path == path2
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isHidden(Path path) throws IOException {
|
||||
return path.getFileName().startsWith('.')
|
||||
}
|
||||
|
||||
@Override
|
||||
FileStore getFileStore(Path path) throws IOException {
|
||||
throw new UnsupportedOperationException("File store not supported by ${getScheme().toUpperCase()} file system provider")
|
||||
}
|
||||
|
||||
@Override
|
||||
void checkAccess(Path path, AccessMode... modes) throws IOException {
|
||||
readAttributes(path, XFileAttributes)
|
||||
|
||||
for( AccessMode m : modes ) {
|
||||
if( m == AccessMode.WRITE )
|
||||
throw new AccessDeniedException("Write mode not supported")
|
||||
if( m == AccessMode.EXECUTE )
|
||||
throw new AccessDeniedException("Execute mode not supported")
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
def <V extends FileAttributeView> V getFileAttributeView(Path path, Class<V> type, LinkOption... options) {
|
||||
return null
|
||||
}
|
||||
|
||||
@Override
|
||||
def <A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type, LinkOption... options) throws IOException {
|
||||
if ( type == BasicFileAttributes || type == XFileAttributes) {
|
||||
def p = (XPath) path
|
||||
def attrs = (A)readHttpAttributes(p)
|
||||
if (attrs == null) {
|
||||
throw new IOException("Unable to access path: ${FilesEx.toUriString(p)}")
|
||||
}
|
||||
return attrs
|
||||
}
|
||||
throw new UnsupportedOperationException("Not a valid ${getScheme().toUpperCase()} file attribute type: $type")
|
||||
}
|
||||
|
||||
@Override
|
||||
Map<String, Object> readAttributes(Path path, String attributes, LinkOption... options) throws IOException {
|
||||
throw new UnsupportedOperationException("Read file attributes not supported by ${getScheme().toUpperCase()} file system provider")
|
||||
}
|
||||
|
||||
@Override
|
||||
void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException {
|
||||
throw new UnsupportedOperationException("Set file attributes not supported by ${getScheme().toUpperCase()} file system provider")
|
||||
}
|
||||
|
||||
protected XFileAttributes readHttpAttributes(XPath path) {
|
||||
final conn = toConnection(path)
|
||||
if( conn instanceof FtpURLConnection ) {
|
||||
return new XFileAttributes(null,-1)
|
||||
}
|
||||
if ( conn instanceof HttpURLConnection && conn.getResponseCode() in [200, 301, 302, 307, 308]) {
|
||||
final header = conn.getHeaderFields()
|
||||
return readHttpAttributes(header)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
protected XFileAttributes readHttpAttributes(Map<String,List<String>> header) {
|
||||
final header0 = InsensitiveMap.<String,List<String>>of(header)
|
||||
def lastMod = header0.get("Last-Modified")?.get(0)
|
||||
long contentLen = header0.get("Content-Length")?.get(0)?.toLong() ?: -1L
|
||||
def dateFormat = new SimpleDateFormat('E, dd MMM yyyy HH:mm:ss Z', Locale.ENGLISH) // <-- make sure date parse is not language dependent (for the week day)
|
||||
def modTime = lastMod ? FileTime.from(dateFormat.parse(lastMod).time, TimeUnit.MILLISECONDS) : (FileTime)null
|
||||
new XFileAttributes(modTime, contentLen)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* Licensed 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 nextflow.file.http
|
||||
|
||||
import java.nio.file.FileSystem
|
||||
import java.nio.file.LinkOption
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.ProviderMismatchException
|
||||
import java.nio.file.WatchEvent
|
||||
import java.nio.file.WatchKey
|
||||
import java.nio.file.WatchService
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
/**
|
||||
* Implements a {@link Path} for http/ftp protocols
|
||||
*
|
||||
* @author Emilio Palumbo <emilio.palumbo@crg.eu>
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
@CompileStatic
|
||||
class XPath implements Path {
|
||||
|
||||
static final public Set<String> ALL_SCHEMES = ['ftp','http','https'] as Set
|
||||
|
||||
private static final String[] EMPTY = []
|
||||
|
||||
private XFileSystem fs
|
||||
|
||||
private Path path
|
||||
|
||||
private String query
|
||||
|
||||
/*
|
||||
* Only needed to prevent serialization issues - see https://github.com/nextflow-io/nextflow/issues/5208
|
||||
*/
|
||||
protected XPath(){}
|
||||
|
||||
XPath(XFileSystem fs, String path) {
|
||||
this(fs, path, EMPTY)
|
||||
}
|
||||
|
||||
XPath(XFileSystem fs, String path, String[] more) {
|
||||
this.fs = fs
|
||||
this.query = query(path)
|
||||
this.path = Paths.get(stripQuery(path) ?:'/', more)
|
||||
}
|
||||
|
||||
private XPath(XFileSystem fs, Path path, String query=null) {
|
||||
this.fs = fs
|
||||
this.path = path
|
||||
this.query = query
|
||||
}
|
||||
|
||||
private URI getBaseUri() {
|
||||
fs?.getBaseUri()
|
||||
}
|
||||
|
||||
private XPath createXPath(String path) {
|
||||
fs && path.startsWith('/') ? new XPath(fs, path) : new XPath(null, path)
|
||||
}
|
||||
|
||||
@Override
|
||||
FileSystem getFileSystem() {
|
||||
return fs
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isAbsolute() {
|
||||
return path.isAbsolute()
|
||||
}
|
||||
|
||||
@Override
|
||||
Path getRoot() {
|
||||
return createXPath("/")
|
||||
}
|
||||
|
||||
@Override
|
||||
Path getFileName() {
|
||||
final result = path?.getFileName()?.toString()
|
||||
return result ? new XPath(null, result) : null
|
||||
}
|
||||
|
||||
@Override
|
||||
Path getParent() {
|
||||
String result = path.parent ? path.parent.toString() : null
|
||||
if( result ) {
|
||||
if( result != '/' ) result += '/'
|
||||
return createXPath(result)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@Override
|
||||
int getNameCount() {
|
||||
return path.toString() ? path.nameCount : 0
|
||||
}
|
||||
|
||||
@Override
|
||||
Path getName(int index) {
|
||||
return new XPath(null, path.getName(index).toString())
|
||||
}
|
||||
|
||||
@Override
|
||||
Path subpath(int beginIndex, int endIndex) {
|
||||
return new XPath(null, path.subpath(beginIndex, endIndex).toString())
|
||||
}
|
||||
|
||||
@Override
|
||||
Path normalize() {
|
||||
return new XPath(fs, path.normalize(), query)
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean startsWith(Path other) {
|
||||
return startsWith(other.toString())
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean startsWith(String other) {
|
||||
return path.startsWith(other)
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean endsWith(Path other) {
|
||||
return endsWith(other.toString())
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean endsWith(String other) {
|
||||
return path.endsWith(other)
|
||||
}
|
||||
|
||||
@Override
|
||||
Path resolve(Path other) {
|
||||
if( this.class != other.class )
|
||||
throw new ProviderMismatchException()
|
||||
|
||||
def that = (XPath)other
|
||||
|
||||
if( that.fs && this.fs != that.fs )
|
||||
return other
|
||||
|
||||
else if( that.path ) {
|
||||
def newPath = this.path.resolve(that.path)
|
||||
return new XPath(fs, newPath, that.query)
|
||||
}
|
||||
else {
|
||||
return this
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
Path resolve(String other) {
|
||||
resolve(get(other))
|
||||
}
|
||||
|
||||
@Override
|
||||
Path resolveSibling(Path other) {
|
||||
if( this.class != other.class )
|
||||
throw new ProviderMismatchException()
|
||||
|
||||
def that = (XPath)other
|
||||
|
||||
if( that.fs && this.fs != that.fs )
|
||||
return other
|
||||
|
||||
if( that.path ) {
|
||||
final Path newPath = this.path.resolveSibling(that.path)
|
||||
return newPath.isAbsolute() ? new XPath(fs, newPath) : new XPath(null, newPath)
|
||||
}
|
||||
else {
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
Path resolveSibling(String other) {
|
||||
resolveSibling(get(other))
|
||||
}
|
||||
|
||||
@Override
|
||||
Path relativize(Path other) {
|
||||
def otherPath = ((XPath)other).path
|
||||
return createXPath(path.relativize(otherPath).toString())
|
||||
}
|
||||
|
||||
@Override
|
||||
URI toUri() {
|
||||
final String concat = query!=null ? "$path?$query" : path.toString()
|
||||
return baseUri ? new URI("$baseUri$concat") : new URI(concat)
|
||||
}
|
||||
|
||||
@Override
|
||||
Path toAbsolutePath() {
|
||||
return this
|
||||
}
|
||||
|
||||
@Override
|
||||
Path toRealPath(LinkOption... options) throws IOException {
|
||||
return this
|
||||
}
|
||||
|
||||
@Override
|
||||
File toFile() {
|
||||
throw new UnsupportedOperationException()
|
||||
}
|
||||
|
||||
@Override
|
||||
String toString() {
|
||||
return path.toString()
|
||||
}
|
||||
|
||||
@Override
|
||||
WatchKey register(WatchService watcher, WatchEvent.Kind<?>[] events, WatchEvent.Modifier... modifiers) throws IOException {
|
||||
throw new UnsupportedOperationException("Register not supported by XFileSystem")
|
||||
}
|
||||
|
||||
@Override
|
||||
WatchKey register(WatchService watcher, WatchEvent.Kind<?>... events) throws IOException {
|
||||
throw new UnsupportedOperationException("Register not supported by XFileSystem")
|
||||
}
|
||||
|
||||
@Override
|
||||
Iterator<Path> iterator() {
|
||||
final len = getNameCount()
|
||||
new Iterator<Path>() {
|
||||
int index
|
||||
Path current = len ? getName(index++) : null
|
||||
|
||||
@Override
|
||||
boolean hasNext() {
|
||||
return current != null
|
||||
}
|
||||
|
||||
@Override
|
||||
Path next() {
|
||||
final result = current
|
||||
current = index<len ? getName(index++) : null
|
||||
return result
|
||||
}
|
||||
|
||||
@Override
|
||||
void remove() {
|
||||
throw new UnsupportedOperationException("Remove operation not supported")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
int compareTo(Path other) {
|
||||
return this.toUri().toString() <=> other.toUri().toString()
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean equals(Object other) {
|
||||
if (other.class != XPath) {
|
||||
return false
|
||||
}
|
||||
final that = (XPath)other
|
||||
return this.fs == that.fs && this.path == that.path && this.query == that.query
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The unique hash code for this path
|
||||
*/
|
||||
@Override
|
||||
int hashCode() {
|
||||
return Objects.hash(fs,path,query)
|
||||
}
|
||||
|
||||
/**
|
||||
* Path factory method.
|
||||
*
|
||||
* @param str
|
||||
* A fully qualified URI path string, e.g. {@code http://www.host.name/data.file.txt}
|
||||
* @return
|
||||
* The corresponding {@link XPath} object
|
||||
* @throws
|
||||
* ProviderMismatchException When the URI scheme does not match any supported protocol, i.e. {@code ftp},
|
||||
* {@code http}, {@code https}
|
||||
*/
|
||||
static XPath get(String str) {
|
||||
if( str == null )
|
||||
return null
|
||||
|
||||
def uri = new URI(null,null,str,null,null)
|
||||
|
||||
if( uri.scheme && !ALL_SCHEMES.contains(uri.scheme))
|
||||
throw new ProviderMismatchException()
|
||||
|
||||
uri.authority ? (XPath)Paths.get(uri) : new XPath(null, str)
|
||||
}
|
||||
|
||||
static String stripQuery(String uri) {
|
||||
if(!uri)
|
||||
return null
|
||||
final p = uri.indexOf('?')
|
||||
return p>0 ? uri.substring(0,p) : (p==0 ? null : uri)
|
||||
}
|
||||
|
||||
static String query(String uri) {
|
||||
if(!uri)
|
||||
return null
|
||||
final p = uri.indexOf('?')
|
||||
return p!=-1 && p<uri.size()-1 ? uri.substring(p+1) : null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* Licensed 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 nextflow.file.http
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
import nextflow.util.SerializerRegistrant
|
||||
import org.pf4j.Extension
|
||||
|
||||
/**
|
||||
* Register the {@link XPathSerializer} as Kryo serializer
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
@Extension
|
||||
@CompileStatic
|
||||
class XPathRegistrant implements SerializerRegistrant {
|
||||
@Override
|
||||
void register(Map<Class, Object> serializers) {
|
||||
serializers.put(XPath, XPathSerializer)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* Licensed 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 nextflow.file.http
|
||||
|
||||
import com.esotericsoftware.kryo.Kryo
|
||||
import com.esotericsoftware.kryo.Serializer
|
||||
import com.esotericsoftware.kryo.io.Input
|
||||
import com.esotericsoftware.kryo.io.Output
|
||||
import groovy.transform.CompileStatic
|
||||
import groovy.util.logging.Slf4j
|
||||
import nextflow.file.FileHelper
|
||||
|
||||
/**
|
||||
* Implements Kryo serializer for {@link XPath}
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
@Slf4j
|
||||
@CompileStatic
|
||||
class XPathSerializer extends Serializer<XPath> {
|
||||
|
||||
@Override
|
||||
void write(Kryo kryo, Output output, XPath target) {
|
||||
final uri = target.toUri().toString()
|
||||
log.trace "XPath serialization > uri=$uri"
|
||||
output.writeString(uri)
|
||||
}
|
||||
|
||||
@Override
|
||||
XPath read(Kryo kryo, Input input, Class<XPath> type) {
|
||||
final uri = input.readString()
|
||||
log.trace "Path de-serialization > uri=$uri"
|
||||
(XPath) FileHelper.asPath(new URI(uri))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#
|
||||
# Copyright 2013-2024, Seqera Labs
|
||||
#
|
||||
# Licensed 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.
|
||||
#
|
||||
|
||||
nextflow.file.http.XPathRegistrant
|
||||
@@ -0,0 +1,19 @@
|
||||
#
|
||||
# Copyright 2013-2026, Seqera Labs
|
||||
#
|
||||
# Licensed 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.
|
||||
#
|
||||
|
||||
nextflow.file.http.FtpFileSystemProvider
|
||||
nextflow.file.http.HttpFileSystemProvider
|
||||
nextflow.file.http.HttpsFileSystemProvider
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* Licensed 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 nextflow.file.http
|
||||
|
||||
import spock.lang.Specification
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
class FixedInputStreamTest extends Specification {
|
||||
|
||||
def 'should read byte by byte' () {
|
||||
given:
|
||||
def bytes = "Hello world". bytes
|
||||
def stream = new FixedInputStream(new ByteArrayInputStream(bytes), bytes.length)
|
||||
|
||||
when:
|
||||
def ch
|
||||
def result = new StringBuilder()
|
||||
while( (ch=stream.read())!=-1 )
|
||||
result.append(ch as char)
|
||||
and:
|
||||
stream.close()
|
||||
then:
|
||||
noExceptionThrown()
|
||||
result.toString() == 'Hello world'
|
||||
}
|
||||
|
||||
def 'should read byte buffer' () {
|
||||
given:
|
||||
def bytes = "Hello world". bytes
|
||||
def stream = new FixedInputStream(new ByteArrayInputStream(bytes), bytes.length)
|
||||
|
||||
when:
|
||||
def buffer = new byte[5]
|
||||
def result = new StringBuilder()
|
||||
def c
|
||||
while( (c=stream.read(buffer))!=-1 ) {
|
||||
for( int i=0; i<c; i++ )
|
||||
result.append(buffer[i] as char)
|
||||
}
|
||||
and:
|
||||
stream.close()
|
||||
then:
|
||||
noExceptionThrown()
|
||||
result.toString() == 'Hello world'
|
||||
}
|
||||
|
||||
def 'should read all bytes' () {
|
||||
given:
|
||||
def bytes = "Hello world". bytes
|
||||
def stream = new FixedInputStream(new ByteArrayInputStream(bytes), bytes.length)
|
||||
|
||||
when:
|
||||
def result = stream.readAllBytes()
|
||||
and:
|
||||
stream.close()
|
||||
then:
|
||||
noExceptionThrown()
|
||||
and:
|
||||
new String(result) == "Hello world"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* Licensed 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 nextflow.file.http
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
|
||||
import com.github.tomakehurst.wiremock.junit.WireMockRule
|
||||
import com.sun.net.httpserver.BasicAuthenticator
|
||||
import com.sun.net.httpserver.Headers
|
||||
import com.sun.net.httpserver.HttpExchange
|
||||
import com.sun.net.httpserver.HttpHandler
|
||||
import com.sun.net.httpserver.HttpServer
|
||||
import groovy.transform.CompileStatic
|
||||
import nextflow.SysEnv
|
||||
import org.junit.Rule
|
||||
import spock.lang.IgnoreIf
|
||||
import spock.lang.Specification
|
||||
import test.TestHelper
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.*
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
class HttpFilesTests extends Specification {
|
||||
|
||||
@Rule
|
||||
WireMockRule wireMockRule = new WireMockRule(0)
|
||||
|
||||
def 'should read http file from WireMock' () {
|
||||
|
||||
given:
|
||||
def localhost = "http://localhost:${wireMockRule.port()}"
|
||||
wireMockRule.stubFor(get(urlEqualTo("/index.html"))
|
||||
.willReturn(aResponse()
|
||||
.withStatus(200)
|
||||
.withBody("""a
|
||||
b
|
||||
c
|
||||
d
|
||||
""")
|
||||
.withHeader("Content-Type", "text/html")
|
||||
.withHeader("Content-Length", "10")
|
||||
.withHeader("Last-Modified", "Fri, 04 Nov 2016 21:50:34 GMT")))
|
||||
|
||||
when:
|
||||
def path = Paths.get(new URI("${localhost}/index.html"))
|
||||
then:
|
||||
Files.size(path) == 10
|
||||
Files.getLastModifiedTime(path).toString() == "2016-11-04T21:50:34Z"
|
||||
|
||||
when:
|
||||
def path2 = Paths.get(new URI("${localhost}missing.html"))
|
||||
then:
|
||||
!Files.exists(path2)
|
||||
}
|
||||
|
||||
def 'should re-try read file' () {
|
||||
given:
|
||||
def RESP = 'Hello world'
|
||||
and:
|
||||
// launch web server
|
||||
def attempt = 0
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress(9900), 0);
|
||||
server.createContext("/", new BasicHandler(RESP, { ++attempt < 3 ? 404 : 200 } ));
|
||||
|
||||
server.start()
|
||||
|
||||
when:
|
||||
def path = Paths.get(new URI('http://admin:Secret1@localhost:9900/foo/bar'))
|
||||
then:
|
||||
path.text == 'Hello world'
|
||||
and:
|
||||
attempt == 3
|
||||
|
||||
cleanup:
|
||||
server?.stop(0)
|
||||
}
|
||||
|
||||
@IgnoreIf({System.getenv('NXF_SMOKE')})
|
||||
def 'read a http file ' () {
|
||||
given:
|
||||
def uri = new URI('http://www.nextflow.io/index.html')
|
||||
when:
|
||||
def path = Paths.get(uri)
|
||||
then:
|
||||
path instanceof XPath
|
||||
|
||||
when:
|
||||
def lines = Files.readAllLines(path, Charset.forName('UTF-8'))
|
||||
then:
|
||||
lines.size()>0
|
||||
lines[0].startsWith('<!DOCTYPE html><html lang="en">')
|
||||
|
||||
}
|
||||
|
||||
@IgnoreIf({System.getenv('NXF_SMOKE')})
|
||||
def 'should check file properties' () {
|
||||
given:
|
||||
SysEnv.push([NXF_HTTPFS_MAX_ATTEMPTS: '1'])
|
||||
|
||||
when:
|
||||
def path1 = Paths.get(new URI('http://www.nextflow.io/index.html'))
|
||||
def path2 = Paths.get(new URI('http://www.google.com/unknown'))
|
||||
def path3 = Paths.get(new URI('http://www.nextflow.io/index.html'))
|
||||
|
||||
then:
|
||||
Files.exists(path1)
|
||||
Files.size(path1) > 0
|
||||
!Files.isDirectory(path1)
|
||||
Files.isReadable(path1)
|
||||
!Files.isExecutable(path1)
|
||||
!Files.isWritable(path1)
|
||||
!Files.isHidden(path1)
|
||||
Files.isRegularFile(path1)
|
||||
!Files.isSymbolicLink(path1)
|
||||
Files.isSameFile(path1, path3)
|
||||
!Files.isSameFile(path1, path2)
|
||||
!Files.exists(path2)
|
||||
|
||||
cleanup:
|
||||
SysEnv.pop()
|
||||
}
|
||||
|
||||
@IgnoreIf({System.getenv('NXF_SMOKE')})
|
||||
def 'should read FTP file' () {
|
||||
when:
|
||||
def lines = Paths.get(new URI('ftp://ftp.ncbi.nlm.nih.gov/robots.txt')).text.readLines()
|
||||
then:
|
||||
lines[0] == 'User-agent: *'
|
||||
lines[1] == 'Disallow: /'
|
||||
}
|
||||
|
||||
@IgnoreIf({System.getenv('NXF_SMOKE')})
|
||||
def 'should read HTTPS file' () {
|
||||
given:
|
||||
def uri = new URI('https://www.nextflow.io/index.html')
|
||||
when:
|
||||
def path = Paths.get(uri)
|
||||
then:
|
||||
path instanceof XPath
|
||||
|
||||
when:
|
||||
def lines = Files.readAllLines(path, Charset.forName('UTF-8'))
|
||||
then:
|
||||
lines.size()>0
|
||||
lines[0].startsWith('<!DOCTYPE html><html lang="en">')
|
||||
}
|
||||
|
||||
@IgnoreIf({System.getenv('NXF_SMOKE')})
|
||||
def 'should copy a file' () {
|
||||
given:
|
||||
def uri = new URI('https://www.nextflow.io/index.html')
|
||||
def source = Paths.get(uri)
|
||||
def target = Files.createTempDirectory('nf').resolve('test.html')
|
||||
|
||||
when:
|
||||
Files.copy(source, target)
|
||||
|
||||
then:
|
||||
source.text == target.text
|
||||
|
||||
cleanup:
|
||||
target?.parent?.deleteDir()
|
||||
}
|
||||
|
||||
@IgnoreIf({System.getenv('NXF_SMOKE')})
|
||||
def 'should read lines' () {
|
||||
given:
|
||||
def uri = new URI('http://www.nextflow.io/index.html')
|
||||
when:
|
||||
def path = Paths.get(uri)
|
||||
then:
|
||||
path instanceof XPath
|
||||
|
||||
when:
|
||||
def str = new String(Files.readAllBytes(path), Charset.forName('UTF-8'))
|
||||
then:
|
||||
str.size()>0
|
||||
str.startsWith('<!DOCTYPE html><html lang="en">')
|
||||
}
|
||||
|
||||
def 'should read with a newByteChannel' () {
|
||||
given:
|
||||
def localhost = "http://localhost:${wireMockRule.port()}"
|
||||
wireMockRule.stubFor(get(urlEqualTo("/index.txt"))
|
||||
.willReturn(aResponse()
|
||||
.withStatus(200)
|
||||
.withBody("01234567890123456789012345678901234567890123456789")
|
||||
.withHeader("Content-Type", "text/html")
|
||||
.withHeader("Content-Length", "50")
|
||||
.withHeader("Last-Modified", "Fri, 04 Nov 2016 21:50:34 GMT")))
|
||||
|
||||
when:
|
||||
def path = Paths.get(new URI("${localhost}/index.txt"))
|
||||
def sbc = Files.newByteChannel(path)
|
||||
def buffer = new ByteArrayOutputStream()
|
||||
ByteBuffer bf = ByteBuffer.allocate(15)
|
||||
while((sbc.read(bf))>0) {
|
||||
bf.flip();
|
||||
buffer.write(bf.array(), 0, bf.limit())
|
||||
bf.clear();
|
||||
}
|
||||
|
||||
then:
|
||||
buffer.toString() == path.text
|
||||
buffer.toString() == '01234567890123456789012345678901234567890123456789'
|
||||
}
|
||||
|
||||
def 'should use basic auth' () {
|
||||
given:
|
||||
def RESP = 'Hello world'
|
||||
and:
|
||||
// launch web server
|
||||
int port = TestHelper.rndServerPort()
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
|
||||
def hc1 = server.createContext("/", new BasicHandler(RESP, 200));
|
||||
|
||||
hc1.setAuthenticator(new BasicAuthenticator("get") {
|
||||
@Override
|
||||
boolean checkCredentials(String user, String pwd) {
|
||||
return user.equals("admin") && pwd.equals("Secret1");
|
||||
} });
|
||||
|
||||
server.start()
|
||||
|
||||
when:
|
||||
def path = Paths.get(new URI("http://admin:Secret1@localhost:${port}/foo/bar"))
|
||||
then:
|
||||
path.text == 'Hello world'
|
||||
|
||||
cleanup:
|
||||
server?.stop(0)
|
||||
}
|
||||
|
||||
@CompileStatic
|
||||
static class BasicHandler implements HttpHandler {
|
||||
|
||||
String body
|
||||
|
||||
Closure<Integer> respCode
|
||||
|
||||
Map<String,String> allHeaders
|
||||
|
||||
BasicHandler(String s, int code, Map<String,String> headers=null) {
|
||||
this.body=s
|
||||
this.respCode = { return code }
|
||||
this.allHeaders = headers ?: Collections.<String,String>emptyMap()
|
||||
}
|
||||
|
||||
BasicHandler(String s, Closure<Integer> code, Map<String,String> headers=null) {
|
||||
this.body=s
|
||||
this.respCode = code
|
||||
this.allHeaders = headers ?: Collections.<String,String>emptyMap()
|
||||
}
|
||||
|
||||
@Override
|
||||
void handle(HttpExchange request) throws IOException {
|
||||
|
||||
Headers header = request.getResponseHeaders()
|
||||
|
||||
for( Map.Entry<String,String> entry : allHeaders.entrySet() ) {
|
||||
header.set(entry.key, entry.value)
|
||||
}
|
||||
|
||||
header.set("Content-Type", "text/plain")
|
||||
request.sendResponseHeaders(respCode.call(), body.size())
|
||||
|
||||
OutputStream os = request.getResponseBody();
|
||||
os.write(body.bytes);
|
||||
os.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* Licensed 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 nextflow.file.http
|
||||
|
||||
|
||||
import spock.lang.Specification
|
||||
import spock.lang.Unroll
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
class XAuthRegistryTest extends Specification {
|
||||
|
||||
@Unroll
|
||||
def 'should register authenticator'() {
|
||||
given:
|
||||
def authenticator = new XAuthRegistry()
|
||||
and:
|
||||
def provider = Mock(XAuthProvider)
|
||||
authenticator.register(provider)
|
||||
and:
|
||||
def conn1 = Mock(URLConnection)
|
||||
|
||||
when:
|
||||
def result = authenticator.authorize(conn1)
|
||||
then:
|
||||
1 * provider.authorize(conn1) >> ACCEPT
|
||||
and:
|
||||
result == EXPECTED
|
||||
|
||||
cleanup:
|
||||
authenticator.unregister(provider)
|
||||
|
||||
where:
|
||||
ACCEPT | EXPECTED
|
||||
false | false
|
||||
true | true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* Licensed 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 nextflow.file.http
|
||||
|
||||
import spock.lang.Specification
|
||||
|
||||
import nextflow.SysEnv
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
class XFileSystemConfigTest extends Specification {
|
||||
|
||||
def 'should create with default config settings' () {
|
||||
when:
|
||||
def config = new XFileSystemConfig()
|
||||
then:
|
||||
config.retryCodes() == XFileSystemConfig.DEFAULT_RETRY_CODES.tokenize(',').collect( it -> it as int )
|
||||
config.backOffDelay() == XFileSystemConfig.DEFAULT_BACK_OFF_DELAY
|
||||
config.backOffBase() == XFileSystemConfig.DEFAULT_BACK_OFF_BASE
|
||||
config.maxAttempts() == XFileSystemConfig.DEFAULT_MAX_ATTEMPTS
|
||||
}
|
||||
|
||||
def 'should create with custom config settings' () {
|
||||
given:
|
||||
SysEnv.push([NXF_HTTPFS_MAX_ATTEMPTS: '10',
|
||||
NXF_HTTPFS_BACKOFF_BASE: '300',
|
||||
NXF_HTTPFS_DELAY : '400',
|
||||
NXF_HTTPFS_RETRY_CODES : '1,2,3'])
|
||||
|
||||
when:
|
||||
def config = new XFileSystemConfig()
|
||||
then:
|
||||
config.retryCodes() == [1,2,3]
|
||||
config.backOffDelay() == 400
|
||||
config.backOffBase() == 300
|
||||
config.maxAttempts() == 10
|
||||
|
||||
cleanup:
|
||||
SysEnv.pop()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* Licensed 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 nextflow.file.http
|
||||
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
|
||||
import com.github.tomakehurst.wiremock.junit.WireMockRule
|
||||
import org.junit.Rule
|
||||
import spock.lang.IgnoreIf
|
||||
import spock.lang.Specification
|
||||
import spock.lang.Unroll
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.*
|
||||
/**
|
||||
* Created by emilio on 08/11/16.
|
||||
*/
|
||||
class XFileSystemProviderTest extends Specification {
|
||||
|
||||
@IgnoreIf({System.getenv('NXF_SMOKE')})
|
||||
def "should return input stream"() {
|
||||
given:
|
||||
def fsp = new HttpFileSystemProvider()
|
||||
def path = fsp.getPath(new URI('http://www.google.com/index.html'))
|
||||
when:
|
||||
def stream = fsp.newInputStream(path)
|
||||
then:
|
||||
stream.text.startsWith("<!doctype html>")
|
||||
}
|
||||
|
||||
def "should return input stream from path"() {
|
||||
given:
|
||||
def DATA = 'Hello world'
|
||||
def fsp = Spy(new HttpFileSystemProvider())
|
||||
def path = fsp.getPath(new URI('http://host.com/index.html?query=123'))
|
||||
def connection = Mock(URLConnection)
|
||||
when:
|
||||
def stream = fsp.newInputStream(path)
|
||||
then:
|
||||
fsp.toConnection(path) >> { Path it ->
|
||||
assert it instanceof XPath
|
||||
assert it.toUri() == new URI('http://host.com/index.html?query=123')
|
||||
return connection
|
||||
}
|
||||
and:
|
||||
connection.getInputStream() >> new ByteArrayInputStream(DATA.bytes)
|
||||
connection.getContentLengthLong() >> DATA.size()
|
||||
and:
|
||||
stream.text == 'Hello world'
|
||||
}
|
||||
|
||||
def "should read file attributes from map"() {
|
||||
given:
|
||||
def fs = new HttpFileSystemProvider()
|
||||
def attrMap = ['last-modified': ['Fri, 04 Nov 2016 21:50:34 GMT'], 'content-length': ['21729']]
|
||||
|
||||
when:
|
||||
def attrs = fs.readHttpAttributes(attrMap)
|
||||
then:
|
||||
attrs.lastModifiedTime().toString() == '2016-11-04T21:50:34Z'
|
||||
attrs.size() == 21729
|
||||
|
||||
when:
|
||||
attrs = fs.readHttpAttributes([:])
|
||||
then:
|
||||
attrs.lastModifiedTime() == null
|
||||
attrs.size() == -1
|
||||
}
|
||||
|
||||
def "should read file attributes with german lang"() {
|
||||
given:
|
||||
def defLocale = Locale.getDefault(Locale.Category.FORMAT)
|
||||
// set german as current language
|
||||
def GERMAN = new Locale.Builder().setLanguage("de").setRegion("DE").build()
|
||||
Locale.setDefault(Locale.Category.FORMAT, GERMAN)
|
||||
def fs = new HttpFileSystemProvider()
|
||||
def attrMap = ['last-modified': ['Fri, 04 Nov 2016 21:50:34 GMT'], 'content-length': ['21729']]
|
||||
|
||||
when:
|
||||
def attrs = fs.readHttpAttributes(attrMap)
|
||||
then:
|
||||
attrs.lastModifiedTime().toString() == '2016-11-04T21:50:34Z'
|
||||
attrs.size() == 21729
|
||||
|
||||
cleanup:
|
||||
Locale.setDefault(Locale.Category.FORMAT, defLocale)
|
||||
}
|
||||
|
||||
@IgnoreIf({System.getenv('NXF_SMOKE')})
|
||||
def "should read file attributes from HttpPath"() {
|
||||
given:
|
||||
def fsp = new HttpFileSystemProvider()
|
||||
def path = (XPath) fsp.getPath(new URI('http://www.nextflow.io/index.html'))
|
||||
|
||||
when:
|
||||
def attrs = fsp.readHttpAttributes(path)
|
||||
then:
|
||||
attrs.lastModifiedTime()
|
||||
attrs.size() > 0
|
||||
}
|
||||
|
||||
@IgnoreIf({System.getenv('NXF_SMOKE')})
|
||||
def "should read file attributes from FtpPath"() {
|
||||
given:
|
||||
def fsp = new FtpFileSystemProvider()
|
||||
def path = (XPath) fsp.getPath(new URI('ftp://ftp.ebi.ac.uk/robots.txt'))
|
||||
|
||||
when:
|
||||
def attrs = fsp.readHttpAttributes(path)
|
||||
then:
|
||||
attrs.lastModifiedTime() == null
|
||||
attrs.size() == -1
|
||||
}
|
||||
|
||||
@Unroll
|
||||
def 'should get uri path'() {
|
||||
given:
|
||||
def provider = new HttpFileSystemProvider()
|
||||
|
||||
when:
|
||||
def path = provider.getPath(new URI(PATH))
|
||||
then:
|
||||
path.toUri().toString() == EXPECTED
|
||||
|
||||
where:
|
||||
PATH | EXPECTED
|
||||
'http://foo.com/this/that' | 'http://foo.com/this/that'
|
||||
'http://FOO.com/this/that' | 'http://foo.com/this/that'
|
||||
'http://MrXYZ@foo.com/this/that' | 'http://MrXYZ@foo.com/this/that'
|
||||
'http://MrXYZ@FOO.com/this/that' | 'http://MrXYZ@foo.com/this/that'
|
||||
'http://@FOO.com/this/that' | 'http://@foo.com/this/that'
|
||||
'http://foo.com/this/that?foo=1' | 'http://foo.com/this/that?foo=1'
|
||||
}
|
||||
|
||||
@Unroll
|
||||
def 'should encode user info'() {
|
||||
given:
|
||||
def provider = new HttpFileSystemProvider()
|
||||
expect:
|
||||
provider.auth(USER_INFO) == EXPECTED
|
||||
where:
|
||||
USER_INFO | EXPECTED
|
||||
"foo:bar" | "Basic ${'foo:bar'.bytes.encodeBase64()}"
|
||||
"x-oauth-bearer:12345" | "Bearer 12345"
|
||||
}
|
||||
|
||||
@Rule
|
||||
WireMockRule wireMockRule = new WireMockRule(0)
|
||||
|
||||
@Unroll
|
||||
def 'should follow a redirect when read a http file '() {
|
||||
given:
|
||||
def localhost = "http://localhost:${wireMockRule.port()}"
|
||||
wireMockRule.stubFor(get(urlEqualTo("/index.html"))
|
||||
.willReturn(aResponse()
|
||||
.withStatus(HTTP_CODE)
|
||||
.withHeader("Location", "${localhost}${REDIRECT_TO}")))
|
||||
|
||||
wireMockRule.stubFor(get(urlEqualTo("/index2.html"))
|
||||
.willReturn(aResponse()
|
||||
.withStatus(HTTP_CODE)
|
||||
.withHeader("Location", "${localhost}/target.html")))
|
||||
|
||||
wireMockRule.stubFor(get(urlEqualTo("/target.html"))
|
||||
.willReturn(aResponse()
|
||||
.withStatus(200)
|
||||
.withBody("""a
|
||||
b
|
||||
c
|
||||
d
|
||||
""")
|
||||
.withHeader("Content-Type", "text/html")
|
||||
.withHeader("Content-Length", "10")
|
||||
.withHeader("Last-Modified", "Fri, 04 Nov 2016 21:50:34 GMT")))
|
||||
|
||||
and:
|
||||
def provider = new HttpFileSystemProvider()
|
||||
when:
|
||||
def path = provider.getPath(new URI("${localhost}/index.html"))
|
||||
then:
|
||||
path
|
||||
Files.size(path) == EXPECTED
|
||||
|
||||
where:
|
||||
HTTP_CODE | REDIRECT_TO | EXPECTED
|
||||
301 | "/target.html" | 10
|
||||
301 | "/index2.html" | 10
|
||||
|
||||
302 | "/target.html" | 10
|
||||
302 | "/index2.html" | 10
|
||||
|
||||
307 | "/target.html" | 10
|
||||
307 | "/index2.html" | 10
|
||||
|
||||
308 | "/target.html" | 10
|
||||
308 | "/index2.html" | 10
|
||||
//infinite redirect to himself
|
||||
308 | "/index.html" | -1
|
||||
}
|
||||
|
||||
def 'should normalize location' () {
|
||||
given:
|
||||
def provider = Spy(XFileSystemProvider)
|
||||
|
||||
expect:
|
||||
provider.absLocation(LOCATION, new URL(TARGET)) == EXPECTED
|
||||
|
||||
where:
|
||||
LOCATION | TARGET | EXPECTED
|
||||
'https://this/that' | 'http://foo.com:123' | 'https://this/that'
|
||||
'/' | 'http://foo.com:123' | 'http://foo.com:123/'
|
||||
'/this/that' | 'http://foo.com:123' | 'http://foo.com:123/this/that'
|
||||
'/this/that' | 'http://foo.com:123/abc' | 'http://foo.com:123/this/that'
|
||||
'this/that' | 'http://foo.com:123/abc' | 'http://foo.com:123/this/that'
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* Licensed 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 nextflow.file.http
|
||||
|
||||
import spock.lang.Specification
|
||||
import spock.lang.Unroll
|
||||
|
||||
/**
|
||||
* @author Emilio Palumbo
|
||||
* @author Paolo DiTommaso
|
||||
*
|
||||
*/
|
||||
class XPathTest extends Specification {
|
||||
|
||||
def 'should validate equals and hashCode' () {
|
||||
|
||||
when:
|
||||
def p1 = XPath.get('http://www.nextflow.io/a/b/c.txt')
|
||||
def p2 = XPath.get('http://www.nextflow.io/a/b/c.txt')
|
||||
def p3 = XPath.get('http://www.nextflow.io/z.txt')
|
||||
def p4 = XPath.get('http://www.google.com/a/b/c.txt')
|
||||
|
||||
then:
|
||||
p1 == p2
|
||||
p1 != p3
|
||||
p1 != p4
|
||||
p1.equals(p2)
|
||||
!p1.equals(p3)
|
||||
!p1.equals(p4)
|
||||
p1.hashCode() == p2.hashCode()
|
||||
p1.hashCode() != p3.hashCode()
|
||||
p1.hashCode() != p4.hashCode()
|
||||
}
|
||||
|
||||
|
||||
def 'should convert to a string' () {
|
||||
expect:
|
||||
XPath.get('http://www.nextflow.io/abc/d.txt').toString()== '/abc/d.txt'
|
||||
and:
|
||||
XPath.get('http://www.nextflow.io/abc/d.txt').toUri() == new URI('http://www.nextflow.io/abc/d.txt')
|
||||
XPath.get('http://www.nextflow.io/abc/d.txt').toUri().toString() == 'http://www.nextflow.io/abc/d.txt'
|
||||
and:
|
||||
XPath.get('http://www.nextflow.io/abc/d.txt?q=1').toUri() == new URI('http://www.nextflow.io/abc/d.txt?q=1')
|
||||
XPath.get('http://www.nextflow.io/abc/d.txt?q=1').toUri().toString() == 'http://www.nextflow.io/abc/d.txt?q=1'
|
||||
and:
|
||||
XPath.get('http://www.nextflow.io/abc/d.txt?').toUri().toString() == 'http://www.nextflow.io/abc/d.txt'
|
||||
}
|
||||
|
||||
def 'should validate uri' () {
|
||||
when:
|
||||
def uri = XPath.get(LOCATION).toUri()
|
||||
then:
|
||||
uri.authority == AUTH
|
||||
uri.path == PATH
|
||||
uri.query == QUERY
|
||||
uri.scheme == SCHEME
|
||||
|
||||
where:
|
||||
LOCATION | SCHEME | AUTH | PATH | QUERY
|
||||
'http://www.nextflow.io/abc/d.txt' | 'http' | 'www.nextflow.io' | '/abc/d.txt' | null
|
||||
'http://www.nextflow.io/abc/d.txt?q=1' | 'http' | 'www.nextflow.io' | '/abc/d.txt' | 'q=1'
|
||||
}
|
||||
|
||||
def "should return url root"() {
|
||||
|
||||
expect:
|
||||
XPath.get(origin).getRoot() == XPath.get(root)
|
||||
XPath.get(origin).getRoot().toString() == '/'
|
||||
XPath.get(origin).getRoot().toUri() == new URI(uri)
|
||||
|
||||
where:
|
||||
origin | root | path | uri
|
||||
'http://www.google.com/abc.txt' | 'http://www.google.com/' | '/' | 'http://www.google.com/'
|
||||
'http://www.google.com/' | 'http://www.google.com/' | '/' | 'http://www.google.com/'
|
||||
'http://www.google.com' | 'http://www.google.com/' | '/' | 'http://www.google.com/'
|
||||
}
|
||||
|
||||
@Unroll
|
||||
def 'should return file name from url' () {
|
||||
|
||||
expect:
|
||||
XPath.get(ORIGIN)?.getFileName() == XPath.get(FILE_NAME)
|
||||
XPath.get(ORIGIN)?.getFileName()?.toString() == FILE_NAME
|
||||
|
||||
where:
|
||||
ORIGIN | FILE_NAME
|
||||
'http://nextflow.io/a/b/c.txt' | 'c.txt'
|
||||
'http://nextflow.io/alpha' | 'alpha'
|
||||
'http://nextflow.io/' | null
|
||||
'http://nextflow.io' | null
|
||||
'http://nextflow.io/a/b/file.txt?q=1'| 'file.txt'
|
||||
}
|
||||
|
||||
|
||||
def 'should return if it is an absolute path'() {
|
||||
|
||||
expect:
|
||||
XPath.get(origin).isAbsolute() == expected
|
||||
|
||||
where:
|
||||
origin | expected
|
||||
'http://nextflow.io/a/b/c.txt' | true
|
||||
'http://nextflow.io/a/b/' | true
|
||||
'name.txt' | false
|
||||
|
||||
}
|
||||
|
||||
def 'should return parent path' () {
|
||||
|
||||
expect:
|
||||
XPath.get(origin).getParent() == XPath.get(parent)
|
||||
|
||||
where:
|
||||
origin | parent
|
||||
'http://nextflow.io/a/b/c.txt' | 'http://nextflow.io/a/b/'
|
||||
'http://nextflow.io/a/' | 'http://nextflow.io/'
|
||||
'http://nextflow.io/a' | 'http://nextflow.io/'
|
||||
'http://nextflow.io/' | null
|
||||
}
|
||||
|
||||
|
||||
@Unroll
|
||||
def 'should return name count #origin' () {
|
||||
|
||||
expect:
|
||||
XPath.get(origin).getNameCount() == count
|
||||
|
||||
where:
|
||||
origin | count
|
||||
'http://nextflow.io/a/b/c.txt' | 3
|
||||
'http://nextflow.io/a/b/' | 2
|
||||
'http://nextflow.io/a/b' | 2
|
||||
'http://nextflow.io/a/' | 1
|
||||
'http://nextflow.io/' | 0
|
||||
'http://nextflow.io' | 0
|
||||
'hello/world' | 2
|
||||
'hello' | 1
|
||||
|
||||
}
|
||||
|
||||
def 'should return name part by index' () {
|
||||
expect:
|
||||
XPath.get('http://nextflow.io/a/b/c.txt').getName(0) == XPath.get('a')
|
||||
XPath.get('http://nextflow.io/a/b/c.txt').getName(1) == XPath.get('b')
|
||||
XPath.get('http://nextflow.io/a/b/c.txt').getName(2) == XPath.get('c.txt')
|
||||
|
||||
when:
|
||||
XPath.get('http://nextflow.io/a/b/c.txt').getName(3)
|
||||
then:
|
||||
thrown(IllegalArgumentException)
|
||||
|
||||
}
|
||||
|
||||
def 'should return a subpath' () {
|
||||
|
||||
expect:
|
||||
XPath.get('http://nextflow.io/a/b/c/d.txt').subpath(0,3) == XPath.get('a/b/c' )
|
||||
XPath.get('http://nextflow.io/a/b/c/d.txt').subpath(1,3) == XPath.get('b/c' )
|
||||
XPath.get('http://nextflow.io/a/b/c/d.txt').subpath(3,4) == XPath.get('d.txt' )
|
||||
}
|
||||
|
||||
@Unroll
|
||||
def 'should resolve a path: #base' () {
|
||||
|
||||
expect:
|
||||
XPath.get(base).resolve(ext) == XPath.get(expected)
|
||||
XPath.get(base).resolve(XPath.get(ext)) == XPath.get(expected)
|
||||
|
||||
where:
|
||||
base | ext | expected
|
||||
'http://nextflow.io/abc' | 'd.txt' | 'http://nextflow.io/abc/d.txt'
|
||||
'http://nextflow.io/abc/' | 'd.txt' | 'http://nextflow.io/abc/d.txt'
|
||||
'http://nextflow.io' | 'file.txt' | 'http://nextflow.io/file.txt'
|
||||
'http://nextflow.io/' | 'file.txt' | 'http://nextflow.io/file.txt'
|
||||
'alpha' | 'beta.txt' | 'alpha/beta.txt'
|
||||
'/alpha' | 'beta.txt' | '/alpha/beta.txt'
|
||||
'http://nextflow.io/abc/' | '/z.txt' | 'http://nextflow.io/z.txt'
|
||||
'http://nextflow.io/abc/' | 'http://x.com/z.txt' | 'http://x.com/z.txt'
|
||||
and:
|
||||
'http://nextflow.io/abc/' | 'http://x.com/z.txt?q=1' | 'http://x.com/z.txt?q=1'
|
||||
'http://nextflow.io/abc/' | 'z.txt?q=1' | 'http://nextflow.io/abc/z.txt?q=1'
|
||||
|
||||
}
|
||||
|
||||
@Unroll
|
||||
def 'should resolve sibling: #base' () {
|
||||
|
||||
expect:
|
||||
XPath.get(base).resolveSibling(ext) == XPath.get(expected)
|
||||
XPath.get(base).resolveSibling(XPath.get(ext)) == XPath.get(expected)
|
||||
|
||||
where:
|
||||
base | ext | expected
|
||||
'http://nextflow.io/ab/c' | 'd.txt' | 'http://nextflow.io/ab/d.txt'
|
||||
'http://nextflow.io/ab/c/' | 'd.txt' | 'http://nextflow.io/ab/d.txt'
|
||||
'http://nextflow.io/ab' | 'd.txt' | 'http://nextflow.io/d.txt'
|
||||
'http://nextflow.io/ab/' | 'd.txt' | 'http://nextflow.io/d.txt'
|
||||
'http://nextflow.io/' | 'd.txt' | 'd.txt'
|
||||
'http://nextflow.io/a/b/c/' | '/p/q.txt' | 'http://nextflow.io/p/q.txt'
|
||||
'http://nextflow.io/abc/' | 'http://x.com/z.txt' | 'http://x.com/z.txt'
|
||||
}
|
||||
|
||||
def 'should normalise a path: #path' () {
|
||||
expect:
|
||||
XPath.get(path).normalize() == XPath.get(expected)
|
||||
|
||||
where:
|
||||
path | expected
|
||||
'http://nextflow.io/ab/c.txt' | 'http://nextflow.io/ab/c.txt'
|
||||
'http://nextflow.io/ab/c/../d.txt' | 'http://nextflow.io/ab/d.txt'
|
||||
'ab/c/../d.txt' | 'ab/d.txt'
|
||||
'http://nextflow.io/ab/c.txt?q=1' | 'http://nextflow.io/ab/c.txt?q=1'
|
||||
|
||||
}
|
||||
|
||||
def 'should iterate over a path' () {
|
||||
|
||||
when:
|
||||
def itr = XPath.get('http://nextflow.io/a/b/c.txt').iterator()
|
||||
then:
|
||||
itr.hasNext()
|
||||
itr.next() == XPath.get('a')
|
||||
itr.hasNext()
|
||||
itr.next() == XPath.get('b')
|
||||
itr.hasNext()
|
||||
itr.next() == XPath.get('c.txt')
|
||||
!itr.hasNext()
|
||||
itr.next() == null
|
||||
}
|
||||
|
||||
@Unroll
|
||||
def 'should fetch query' () {
|
||||
expect:
|
||||
XPath.query(PATH) == EXPECTED
|
||||
|
||||
where:
|
||||
PATH | EXPECTED
|
||||
null | null
|
||||
'foo.txt' | null
|
||||
'foo?x=1' | 'x=1'
|
||||
'foo?x=1&y=2' | 'x=1&y=2'
|
||||
'foo?' | null
|
||||
'?x' | 'x'
|
||||
}
|
||||
|
||||
@Unroll
|
||||
def 'should strip query' () {
|
||||
expect:
|
||||
XPath.stripQuery(PATH) == EXPECTED
|
||||
|
||||
where:
|
||||
PATH | EXPECTED
|
||||
null | null
|
||||
'foo.txt' | 'foo.txt'
|
||||
'foo?x=1' | 'foo'
|
||||
'foo?x=1&y=2' | 'foo'
|
||||
'foo?' | 'foo'
|
||||
'?x' | null
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user