forked from direnv/direnv
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcmd_fetchurl.go
More file actions
139 lines (117 loc) · 3.31 KB
/
cmd_fetchurl.go
File metadata and controls
139 lines (117 loc) · 3.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
package main
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/direnv/direnv/sri"
"github.com/mattn/go-isatty"
)
// CmdFetchURL is `direnv fetchurl <url> [<integrity-hash>]`
var CmdFetchURL = &Cmd{
Name: "fetchurl",
Desc: "Fetches a given URL into direnv's CAS",
Args: []string{"<url>", "[<integrity-hash>]"},
Action: actionWithConfig(cmdFetchURL),
}
func cmdFetchURL(env Env, args []string, config *Config) (err error) {
if len(args) < 2 {
return fmt.Errorf("missing URL argument")
}
var (
algo sri.Algo = sri.SHA256
url string
integrityHash string
)
casDir := casDir(config)
isTTY := isatty.IsTerminal(os.Stdout.Fd())
url = args[1]
// Validate the SRI hash if it exists
if len(args) > 2 {
// Support Base64 where '/' have been replaced by '_'
integrityHash = strings.ReplaceAll(args[2], "_", "/")
algo, err = sri.GetAlgo(integrityHash)
if err != nil {
return err
}
// Shortcut if the cache already has the file
casFile := casPath(casDir, integrityHash)
if fileExists(casFile) {
fmt.Println(casFile)
return nil
}
}
// Create the CAS directory if it doesn't exist
if err = os.MkdirAll(casDir, os.FileMode(0755)); err != nil {
return err
}
// Create a temporary file to copy the content into, before the CAS file
// location can be calculated.
tmpfile, err := ioutil.TempFile(casDir, "tmp")
if err != nil {
return err
}
defer os.Remove(tmpfile.Name()) // clean up
// Get the URL
// /* #nosec */
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
// While copying the content into the temporary location, also calculate the
// SRI hash.
w := sri.NewWriter(tmpfile, algo)
if _, err = io.Copy(w, resp.Body); err != nil {
return err
}
// Here is the new SRI hash
calculatedHash := w.Sum()
// Make the file read-only and executable for later
if err = os.Chmod(tmpfile.Name(), os.FileMode(0500)); err != nil {
return err
}
// Validate if a comparison hash was given
if integrityHash != "" && calculatedHash != integrityHash {
return fmt.Errorf("hash mismatch. Expected '%s' but got '%s'", integrityHash, calculatedHash)
}
// Derive the CAS file location from the SRI hash
casFile := casPath(casDir, calculatedHash)
// Put the file into the CAS store if it's not already there
if !fileExists(casFile) {
// Move the temporary file to the CAS location.
if err = os.Rename(tmpfile.Name(), casFile); err != nil {
return err
}
}
if integrityHash == "" {
if isTTY {
// Print an example for terminal users
fmt.Printf(`Found hash: %s
Invoke fetchurl again with the hash as an argument to get the disk location:
direnv fetchurl "%s" "%s"
#=> %s
`, calculatedHash, url, calculatedHash, casFile)
} else {
// Only print the hash in scripting mode. Add one extra hurdle on
// purpose to use fetchurl without the SRI hash.
_, err = fmt.Println(calculatedHash)
}
} else {
// Print the location to the CAS file
_, err = fmt.Println(casFile)
}
return err
}
func casDir(c *Config) string {
return filepath.Join(c.CacheDir, "cas")
}
// casPath returns filesystem path for SRI hashes
func casPath(dir string, integrityHash string) string {
// avoid / in the filename
sriFile := strings.ReplaceAll(integrityHash, "/", "_")
return filepath.Join(dir, sriFile)
}