Posted to tcl by dbohdan at Wed May 05 06:35:00 GMT 2021view pretty

#! /usr/bin/env tclsh
# Copyright (c) 2021 D. Bohdan and contributors.
# License: MIT.
# ==============================================================================
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
# ==============================================================================

package require Tcl 8.7


namespace eval ::pkgfs {
    variable available {
        {core tcllib 1.20+}
        {
            ref
            QmRjsBWcu4W78F19vkXeBgwY5m8fpD2A6SHeSCCsAnHh5B

            checksum
            92479b4e7beb8e2682dac733fd5399252f00fe2219d5532d9065aeb634360c49

            algo
            sha256
        }

        {dbohdan autoopts 0.6.1}
        {
            ref
            Qmea9ksHVd8sGTDuREX3jvgeymggnkE3eaWUkXEnS6Y5cH

            checksum
            0102ef871955d26ba17c42d0ec4fcb0e32f67c4b5f1a7b12880456dba199c095

            algo
            sha256
        }

        {dbohdan jimlib 0.15.0}
        {
            ref
            QmSoxa3WhHdDUHQo7JjD4DVn5sUM9sxfDUrSQbhdBNuipQ

            checksum
            b413d8938818c4b100b968096a45a0686465bdd53392480a5bdb89ff11fb8329

            algo
            sha256
        }

        {de tdom 0.9.1 x86_64-linux-gnu}
        {
            ref
            QmcC3T579hjjVb88VnM6DzTgSTbNNoqtSdvKzasRfTvCzP

            checksum
            fbea1a7654b667047fbee42ae5a96d3bc4c4213f0a692dea9120cf102d5887d4

            algo
            sha256
        }

        {drh wapp 1.0.0}
        {
            ref
            QmUSNZqcQZUunP9gRn2kibz4TqAw1WAzXpdPizBFfQDFEq

            checksum
            86bbb01571ff21db275593d5366febd67fd47635b2c1db6085ef2db257b05c3f

            algo
            sha256
        }

        {mpcjanssen fcgi.tcl 0.5}
        {
            ref
            QmeiVCPoBvk9KMU85vKkoAZWekFWggjAp2iAYPkQQpkqi7

            checksum
            8dd51684a42b489e53b5636280f435fa8161b1a6c51dec766fe87273a85d4a1d

            algo
            sha256
        }
    }
    variable debug [dict getdef [array get ::env] PKGFS_DEBUG false]
    variable version 0.2.0
}

namespace eval ::pkgfs::checksum {
    variable command
    set command(sha256) {}
}

namespace eval ::pkgfs::http {
    variable client {}
    variable timeout 60
}

namespace eval ::pkgfs::ipfs {
    variable cacheDir [file normalize ~/.cache/pkgfs/]
    variable currentGateway 0
    variable gateways {
        https://cloudflare-ipfs.com/ipfs/%s
        https://ipfs.io/ipfs/%s
    }
}


proc ::pkgfs::configure {} {
    detect {
        package require http
        package require tls

        http::register https 443 [list ::tls::socket -autoservername true]

        set ::pkgfs::http::client tcl-http
    } {
        exec curl --version
        set ::pkgfs::http::client curl-cli
    } {
        try {
            exec busybox wget
        } on error e {
            if {![string match *--continue* $e]} {
                error {can't execure busybox wget}
            }
        }

        set ::pkgfs::http::client busybox-wget
    } {
        error {no usable HTTPS client}
    }

    detect {
        exec sha256sum << {}
        set ::pkgfs::checksum::command(sha256sum) sha256sum
    } {
        exec busybox sha256sum << {}
        set ::pkgfs::checksum::command(sha256sum) {busybox sha256sum}
    } {
        error {no usable sha256sum}
    }
}


proc ::pkgfs::detect args {
    set errorScript [lindex $args end]
    set args [lrange $args 0 end-1]
    set ok false

    foreach script $args {
        try {
            uplevel 1 $script
        } on ok _ {
            set ok true
            break
        } on error _ {}
    }

    if {!$ok} {
        uplevel 1 $errorScript
    }
}

proc ::pkgfs::grab-all {carts {tries 3}} {
    foreach cart $carts {
        grab $cart $tries
    }
}


proc ::pkgfs::grab {cart {tries 3}} {
    set pluggedIn false
    lassign [name-to-ref $cart] metadata fullName
    debug Grabbing cart $cart
    debug Full name is $fullName
    debug Ref is [dict get $metadata ref]

    for {set i 1} {$i <= $tries} {incr i} {
        debug Attempt number $i

        try {
            set f [ipfs::cache [dict get $metadata ref]]
            debug Cart is in $f
            plug-in $f
        } trap {PKGFS HTTP DOWNLOAD} e {
            debug HTTP client error: $e
        } trap {TCL ZIPFS} e {
            debug zipfs error: $e
            file delete $f
        } on ok _ {
            set pluggedIn true
            break
        }
    }

    if {!$pluggedIn} {
        return \
            -code error \
            -errorcode {PKGFS GRAB} \
            [list failed to download and plug in cart $cart] \
    }

    debug inb4
    set algo [dict get $metadata algo]
    set actual [checksum::$algo $f]
    debug done
    set expected [dict get $metadata checksum]
    if {$actual ne $expected} {
        return \
            -code error \
            -errorcode {PKGFS GRAB} \
            [list wrong $algo checksum $actual for cart $cart]
    }

    debug Plugged in $f
}


proc ::pkgfs::debug args {
    variable debug

    if {!$debug} return
    set caller [dict get [info frame -1] proc]
    puts stderr [format {PKGFS %-25s %s} $caller $args]
}


proc ::pkgfs::name-to-ref cart {
    variable available

    set len [llength $cart]

    foreach k [dict keys $available] {
        if {[lrange $k 0 $len-1] eq $cart} {
            return [list [dict get $available $k] $k]
        }
    }

    error [list unknown cart: $cart]
}


proc ::pkgfs::plug-in zip {
    set new [index-dirs [mount $zip]]
    lappend ::auto_path {*}$new

    return $new
}


proc ::pkgfs::mount zip {
    set root [zipfs root]
    set dest $root[file rootname [file tail $zip]]

    zipfs mount $dest $zip

    return $dest
}


proc ::pkgfs::index-dirs path {
    lmap f [zipfs list -glob $path*] {
        if {[file tail $f] ne {pkgIndex.tcl}} continue
        file dirname $f
    }
}


proc ::pkgfs::checksum::sha256 path {
    variable command

    lindex [exec {*}$command(sha256sum) $path] 0
}


proc ::pkgfs::ipfs::cache src {
    variable cacheDir

    file mkdir $cacheDir
    set dest [file join $cacheDir $src]

    if {[file exists $dest]} {
        ::pkgfs::debug Destination exists
        return $dest
    }

    ::pkgfs::debug Downloading
    download $src $dest

    return $dest
}


proc ::pkgfs::ipfs::download {src dest} {
    variable currentGateway
    variable gateways

    set gateway [lindex $gateways $currentGateway]
    set currentGateway [expr { ($currentGateway + 1) % [llength $gateways] }]
    ::pkgfs::debug Chose gateway $gateway
    ::pkgfs::http::download [format $gateway $src] $dest
}


proc ::pkgfs::http::download {url dest} {
    variable client

    try {
        ::pkgfs::debug Download client: $client
        $client $url $dest
    } on error {e opts} {
        dict set opts -errorcode {PKGFS HTTP DOWNLOAD}
        return -code error {*}$opts $e
    }
}


proc ::pkgfs::http::busybox-wget {url dest} {
    variable timeout

    # BusyBox wget never checks certificates.
    exec busybox wget \
        --no-check-certificate \
        -O $dest \
        -q \
        $url \
}


proc ::pkgfs::http::curl-cli {url dest} {
    variable timeout

    exec curl \
        --fail \
        --location \
        --max-time $timeout \
        --output $dest \
        --silent \
        $url \
}


proc ::pkgfs::http::tcl-http {src dest} {
    variable timeout

    try {
        set token [http::get $url -timeout ${timeout}000
        set data [http::data $token]

        set ch [open $dest wb]
        puts -nonewline $ch $data
    } finally {
        catch { http::cleanup $token }
        catch { close $ch }
    }
}


::pkgfs::configure

package provide pkgfs 0