Dowload file in one task - pass to another

Evening,

I am no Gradle expert and mostly just muddle through. However I have hit on a rather simple looking problem that I can’t solve to my satisfaction.

I have a task that downloads a Zip file. I did take the approach of downloading and unzipping the file but was surprised to learn that Copy tasks output is the containing folder not its contents.

I then took what I thought was a simpler approach of having one task download the file and I can unzip in the classes that require the contents.

tasks.register('download') {
    def fileName = "special.zip"
    outputs.file "${layout.buildDirectory.dir("download")}/${fileName}"
    String url = "http://someserver/" + fileName
    def download = {
        def file = new File(temporaryDir, fileName)
        new URL(url).withInputStream {i ->
            file.withOutputStream{ it << i}
        }
        file
    }

    copy {
        from (download)
        into layout.buildDirectory.dir("download")
    }
}

tasks.register('runCommand') {
    def filesToGenerateFrom = file(download)
    println filesToGenerateFrom
}

My expectation is that running gradle runCommand would then download the zip and display the filepath (or similar). I would then hope re-running the command would detect the file is downloaded and not bother downloading it again. The file is downloaded however I get an error:

> Could not create task ':runCommand'.
   > Cannot convert the provided notation to a File or URI: task ':download'.
     The following types/formats are supported:
       - A String or CharSequence path, for example 'src/main/java' or '/usr/include'.
       - A String or CharSequence URI, for example 'file:/usr/include'.
...

It seems like a simple enough task (excuse the pun) but I just can’t seem to figure out what is required. Would someone be able to point me in the right direction?

Maybe you should have a look at the Undercouch Download Gradle plugin which provides tasks to download files conveniently.

Besides that what you do there looks really from behind, through the chest, into the eye.

And more importantly you don’t do anything when the tasks are executed, actually they will always be skipped as they have no action at all.
All you do, you do during their configuration time, so when they are configured, no matter if and when they would be executed or not.
To do some action at task execution time for an ad-hoc task you try to create there, you have to do it in a doLast { ... } or doFirst { ... }.

And the error you get is quite semantic, isn’t it?
You call file(...) with an unsupported argument.
A task can have many output files, you what you maybe would have wanted was files(download) for example.

Where I’m from its called “arse about face” and is my modus operandi. Not defining a doLast {…}block is particularly stupid.

The errors maybe semantic but I’ve always found error messages presume some amount of experience.

My second attempt is a bit more successful. I can download the file and the runCommand task demonstrates I am able to operate on the file from the previous task. A mistake I didn’t realise was that copy into assignment needs to be identical to that in output.file.

tasks.register('download') {
    def fileName = "file.zip"
    String url = "http://someserver" + fileName
    def downloadDir = layout.buildDirectory.dir("download")
    def outputFile = "${downloadDir}/${fileName}"
    outputs.file outputFile
    doLast {
        def download = {
            def file = new File(temporaryDir, fileName)
            new URL(url).withInputStream { i ->
                file.withOutputStream { it << i }
            }
            file
        }
        println "Downloading"
        copy {
            from download
            into outputFile
        }
    }
}

tasks.register('runCommand', Copy) {
    from download
    into layout.buildDirectory.dir("other")
}

My next challenge would be to unzip the downloaded file. I thought zipTree would be perfect.

tasks.register('runCommand', Copy) {
    from zipTree(download)
    into layout.buildDirectory.dir("other")
}

But again, my nemesis has returned:

> Cannot convert the provided notation to a File or URI: task ':download'.

How can the output definition of “download” be acceptable for the Copy from property - but not adequate for passing to zipTree()? I ask out of frustration but would appreciate any insights.

Same explanation was before :slight_smile:
A task can have multiple output files, so if you use the task “as file(s)”, it is a set of files not one file.

A copy task can copy multiple files to the destination.

But a ziptree can only unzip one archive.

Your quite right, again.

tasks.register('runCommand', Copy) {
    from (download.outputs.files.collect {zipTree(it)})
    into layout.buildDirectory.dir("other")
}

Treating the downloadtask as returning multiple files breaks the implicit dependency between the tasks. Calling clean runCommand now complains there is no zip available. Which makes sense as the download task never executed.

Writing lazy-implicit ad-hoc tasks is a challenge for me. Hence my perseverance with this “simple” example.

Poking and prodding has brought me here.

tasks.register('runCommand') {
    inputs.files(download)
    doLast {
        copy {
            from download.outputs.files.files.collect{zipTree(it)}
            into layout.buildDirectory.dir("other")
        }
    }
}

Which has re-established the dependency between the tasks and unzips the downloaded file.

That being said it still looks … clumsy. The dependency whilst not using dependsOn is more explicit than my original (non-working) Copy task.

Is this the best I can do? Is shall I just try harder?

Actually, by using download you anyway break the precious task-configuration avoidance you try to adhere by using register.

The simple name-based task access in Groovy DSL predates task-configuration avoidance, so is an eager API.
Better use tasks.named or just store the result of calling register to a variable to further work with a task.
Because then you have a task provider which you can then map and thereby also preserve the implicit task dependency.

Btw. I also strongly recommend switching to Kotlin DSL. By now it is the default DSL, you immediately get type-safe build scripts, actually helpful error messages if you mess up the syntax, and amazingly better IDE support if you use a good IDE like IntelliJ IDEA or Android Studio.

Thanks again for your comments. Your information regarding older/newer DSLs has put me en garde with respect to other Gradle tasks/examples I read on Stackoverflow etc. I find examples that seem to be conflicting or not following other layouts etc.

Better use tasks.named or just store the result of calling register to a variable to further work with a task. Because then you have a task provider which you can then map and thereby also preserve the implicit task dependency.

This was very helpful as it helped me understand the suggestions in the documentation.

This has lead to my latest and greatest:

def downloader = tasks.register('download') {
    def fileName = "sample-1.zip"
    String url = "https://getsamplefiles.com/download/zip/" + fileName
    def outFile = layout.buildDirectory.file(fileName)
    outputs.file outFile
    doLast {
        new URL(url).withInputStream { i ->
                    outFile.each {
                        it.get().asFile.withOutputStream { it << i }
                }
        }
    }
}

tasks.register('altRunCommand', Copy) {
    from downloader.map {
        it.outputs.getFiles().collect {zipTree(it)}
    }
    into layout.buildDirectory.dir("example")
}

I have to admit I arrived at the above after writing a custom task.

It might be pride speaking - but I’m happy with this solution. That being said if there are obvious improvements that the cognescenti observe, I’m open to hearing suggestions.

I will now move onto the original problem which was providing the path of an decompressed file to another task.

2 Likes