Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code | Sign in
(260)

Side by Side Diff: state/apiserver/charms.go

Issue 67750045: Implement the get charm file API.
Patch Set: Created 10 years, 1 month ago
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments. Please Sign in to add in-line comments.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2013 Canonical Ltd. 1 // Copyright 2013 Canonical Ltd.
2 // Licensed under the AGPLv3, see LICENCE file for details. 2 // Licensed under the AGPLv3, see LICENCE file for details.
3 3
4 package apiserver 4 package apiserver
5 5
6 import ( 6 import (
7 "archive/zip" 7 "archive/zip"
8 "crypto/sha256" 8 "crypto/sha256"
9 "encoding/base64" 9 "encoding/base64"
10 "encoding/hex" 10 "encoding/hex"
(...skipping 12 matching lines...) Expand all
23 "launchpad.net/juju-core/charm" 23 "launchpad.net/juju-core/charm"
24 envtesting "launchpad.net/juju-core/environs/testing" 24 envtesting "launchpad.net/juju-core/environs/testing"
25 "launchpad.net/juju-core/names" 25 "launchpad.net/juju-core/names"
26 "launchpad.net/juju-core/state" 26 "launchpad.net/juju-core/state"
27 "launchpad.net/juju-core/state/api/params" 27 "launchpad.net/juju-core/state/api/params"
28 "launchpad.net/juju-core/state/apiserver/common" 28 "launchpad.net/juju-core/state/apiserver/common"
29 ) 29 )
30 30
31 // charmsHandler handles charm upload through HTTPS in the API server. 31 // charmsHandler handles charm upload through HTTPS in the API server.
32 type charmsHandler struct { 32 type charmsHandler struct {
33 » state *state.State 33 » state *state.State
34 » dataDir string
34 } 35 }
35 36
36 func (h *charmsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 37 func (h *charmsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
37 if err := h.authenticate(r); err != nil { 38 if err := h.authenticate(r); err != nil {
38 h.authError(w) 39 h.authError(w)
39 return 40 return
40 } 41 }
41 42
42 switch r.Method { 43 switch r.Method {
43 case "POST": 44 case "POST":
45 // Add a local charm to the store provider.
46 // Requires a "series" query specifying the series to use for th e charm.
44 charmURL, err := h.processPost(r) 47 charmURL, err := h.processPost(r)
45 if err != nil { 48 if err != nil {
46 h.sendError(w, http.StatusBadRequest, err.Error()) 49 h.sendError(w, http.StatusBadRequest, err.Error())
47 return 50 return
48 } 51 }
49 h.sendJSON(w, http.StatusOK, &params.CharmsResponse{CharmURL: ch armURL.String()}) 52 h.sendJSON(w, http.StatusOK, &params.CharmsResponse{CharmURL: ch armURL.String()})
50 » // Possible future extensions, like GET. 53 » case "GET":
54 » » // Retrieve or list charm files.
55 » » // Requires "url" (charm URL) and an optional "file" (the path t o the
56 » » // charm file) to be included in the query.
57 » » if bundlePath, filePath, err := h.processGet(r); err != nil {
58 » » » // An error occurred retrieving the charm bundle.
59 » » » h.sendError(w, http.StatusBadRequest, err.Error())
60 » » } else if filePath == "" {
61 » » » // The client requested the list of charm files.
62 » » » h.sendFilesList(w, bundlePath)
63 » » } else {
64 » » » // The client requested a specific file.
65 » » » h.sendFile(w, r, filePath)
66 » » }
51 default: 67 default:
52 h.sendError(w, http.StatusMethodNotAllowed, fmt.Sprintf("unsuppo rted method: %q", r.Method)) 68 h.sendError(w, http.StatusMethodNotAllowed, fmt.Sprintf("unsuppo rted method: %q", r.Method))
53 } 69 }
54 } 70 }
55 71
56 // sendJSON sends a JSON-encoded response to the client. 72 // sendJSON sends a JSON-encoded response to the client.
57 func (h *charmsHandler) sendJSON(w http.ResponseWriter, statusCode int, response *params.CharmsResponse) error { 73 func (h *charmsHandler) sendJSON(w http.ResponseWriter, statusCode int, response *params.CharmsResponse) error {
74 w.Header().Set("Content-Type", "application/json")
58 w.WriteHeader(statusCode) 75 w.WriteHeader(statusCode)
59 body, err := json.Marshal(response) 76 body, err := json.Marshal(response)
60 if err != nil { 77 if err != nil {
61 return err 78 return err
62 } 79 }
63 w.Write(body) 80 w.Write(body)
64 return nil 81 return nil
65 } 82 }
66 83
84 // sendFilesList sends a JSON-encoded response to the client including the list
85 // of files contained in the given path.
86 func (h *charmsHandler) sendFilesList(w http.ResponseWriter, path string) {
87 var files []string
88 err := filepath.Walk(path, func(filePath string, fileInfo os.FileInfo, e rr error) error {
89 if err != nil {
90 return err
91 }
92 // Exclude directories.
93 if fileInfo.IsDir() {
94 return nil
95 }
96 relPath, err := filepath.Rel(path, filePath)
97 if err != nil {
98 return err
99 }
100 files = append(files, relPath)
101 return nil
102 })
103 if err != nil {
104 http.Error(
105 w, fmt.Sprintf("unable to list files in %q: %v", path, e rr),
106 http.StatusInternalServerError)
107 return
108 }
109 h.sendJSON(w, http.StatusOK, &params.CharmsResponse{Files: files})
110 }
111
112 // sendFile sends the file contents of the file present in the given path.
113 // A 404 page not found is returned if path does not exist.
114 // A 403 forbidden error is returned if path points to a directory.
115 func (h *charmsHandler) sendFile(w http.ResponseWriter, r *http.Request, path st ring) {
116 file, err := os.Open(path)
117 if os.IsNotExist(err) {
118 http.NotFound(w, r)
119 return
120 } else if err != nil {
121 http.Error(
122 w, fmt.Sprintf("unable to open file at %q: %v", path, er r),
123 http.StatusInternalServerError)
124 return
125 }
126 defer file.Close()
127 fileInfo, err := file.Stat()
128 if err != nil {
129 http.Error(
130 w, fmt.Sprintf("unable to get info about %q: %v", path, err),
131 http.StatusInternalServerError)
132 return
133 }
134 // Deny directory listing.
135 if fileInfo.IsDir() {
136 http.Error(w, "directory listing not allowed", http.StatusForbid den)
137 return
138 }
139 http.ServeContent(w, r, fileInfo.Name(), fileInfo.ModTime(), file)
140 }
141
67 // sendError sends a JSON-encoded error response. 142 // sendError sends a JSON-encoded error response.
68 func (h *charmsHandler) sendError(w http.ResponseWriter, statusCode int, message string) error { 143 func (h *charmsHandler) sendError(w http.ResponseWriter, statusCode int, message string) error {
69 return h.sendJSON(w, statusCode, &params.CharmsResponse{Error: message}) 144 return h.sendJSON(w, statusCode, &params.CharmsResponse{Error: message})
70 } 145 }
71 146
72 // authenticate parses HTTP basic authentication and authorizes the 147 // authenticate parses HTTP basic authentication and authorizes the
73 // request by looking up the provided tag and password against state. 148 // request by looking up the provided tag and password against state.
74 func (h *charmsHandler) authenticate(r *http.Request) error { 149 func (h *charmsHandler) authenticate(r *http.Request) error {
75 parts := strings.Fields(r.Header.Get("Authorization")) 150 parts := strings.Fields(r.Header.Get("Authorization"))
76 if len(parts) != 2 || parts[0] != "Basic" { 151 if len(parts) != 2 || parts[0] != "Basic" {
(...skipping 29 matching lines...) Expand all
106 func (h *charmsHandler) authError(w http.ResponseWriter) { 181 func (h *charmsHandler) authError(w http.ResponseWriter) {
107 w.Header().Set("WWW-Authenticate", `Basic realm="juju"`) 182 w.Header().Set("WWW-Authenticate", `Basic realm="juju"`)
108 h.sendError(w, http.StatusUnauthorized, "unauthorized") 183 h.sendError(w, http.StatusUnauthorized, "unauthorized")
109 } 184 }
110 185
111 // processPost handles a charm upload POST request after authentication. 186 // processPost handles a charm upload POST request after authentication.
112 func (h *charmsHandler) processPost(r *http.Request) (*charm.URL, error) { 187 func (h *charmsHandler) processPost(r *http.Request) (*charm.URL, error) {
113 query := r.URL.Query() 188 query := r.URL.Query()
114 series := query.Get("series") 189 series := query.Get("series")
115 if series == "" { 190 if series == "" {
116 » » return nil, fmt.Errorf("expected series= URL argument") 191 » » return nil, fmt.Errorf("expected series=URL argument")
117 } 192 }
118 // Make sure the content type is zip. 193 // Make sure the content type is zip.
119 contentType := r.Header.Get("Content-Type") 194 contentType := r.Header.Get("Content-Type")
120 if contentType != "application/zip" { 195 if contentType != "application/zip" {
121 return nil, fmt.Errorf("expected Content-Type: application/zip, got: %v", contentType) 196 return nil, fmt.Errorf("expected Content-Type: application/zip, got: %v", contentType)
122 } 197 }
123 tempFile, err := ioutil.TempFile("", "charm") 198 tempFile, err := ioutil.TempFile("", "charm")
124 if err != nil { 199 if err != nil {
125 return nil, fmt.Errorf("cannot create temp file: %v", err) 200 return nil, fmt.Errorf("cannot create temp file: %v", err)
126 } 201 }
(...skipping 285 matching lines...) Expand 10 before | Expand all | Expand 10 after
412 return errgo.Annotate(err, "cannot parse storage URL") 487 return errgo.Annotate(err, "cannot parse storage URL")
413 } 488 }
414 489
415 // And finally, update state. 490 // And finally, update state.
416 _, err = h.state.UpdateUploadedCharm(archive, curl, bundleURL, bundleSHA 256) 491 _, err = h.state.UpdateUploadedCharm(archive, curl, bundleURL, bundleSHA 256)
417 if err != nil { 492 if err != nil {
418 return errgo.Annotate(err, "cannot update uploaded charm in stat e") 493 return errgo.Annotate(err, "cannot update uploaded charm in stat e")
419 } 494 }
420 return nil 495 return nil
421 } 496 }
497
498 // processGet handles a charm file download GET request after authentication.
499 // It returns the bundle path, the requested file path (if any) and an error.
500 func (h *charmsHandler) processGet(r *http.Request) (string, string, error) {
501 query := r.URL.Query()
502
503 // Retrieve and validate query parameters.
504 curl := query.Get("url")
505 if curl == "" {
506 return "", "", fmt.Errorf("expected url=CharmURL query argument" )
507 }
508 file := query.Get("file")
509
510 // Prepare the bundle directories.
511 name := charm.Quote(curl)
512 bundlePath := filepath.Join(h.dataDir, "charm-get-cache", name)
513 filePath, err := h.getFilePath(bundlePath, file)
514 if err != nil {
515 return "", "", err
516 }
517
518 // Check if the charm bundle is already in the cache.
519 if _, err := os.Stat(bundlePath); os.IsNotExist(err) {
520 // Download the charm and extract the bundle.
521 if err = h.downloadCharm(name, bundlePath); err != nil {
522 return "", "", fmt.Errorf("unable to retrieve and save t he charm: %v", err)
523 }
524 } else if err != nil {
525 return "", "", fmt.Errorf("cannot access the charms cache: %v", err)
526 }
527 return bundlePath, filePath, nil
528 }
529
530 // getFilePath return the absolute path of a charm file, based on the given
531 // bundlePath. It also checks that the resulting path lives inside the bundle.
532 func (h *charmsHandler) getFilePath(bundlePath, file string) (string, error) {
533 if file == "" {
534 return "", nil
535 }
536 filePath, err := filepath.Abs(filepath.Join(bundlePath, file))
537 if err != nil {
538 return "", errgo.Annotate(err, "cannot retrieve the requested pa th")
539 }
540 if !strings.HasPrefix(filePath, bundlePath+"/") {
dimitern 2014/02/25 10:07:09 Please, use filepath.Dir(filePath) != bundlePath h
541 return "", fmt.Errorf("invalid file path: %q", file)
542 }
543 return filePath, nil
544 }
545
546 // downloadCharm downloads the given charm name from the provider storage and
547 // extracts the corresponding bundle to the given bundlePath.
548 func (h *charmsHandler) downloadCharm(name, bundlePath string) error {
549 // Get the provider storage.
550 storage, err := envtesting.GetEnvironStorage(h.state)
551 if err != nil {
552 return errgo.Annotate(err, "cannot access provider storage")
553 }
554
555 // Use the storage to retrieve and save the charm archive.
556 reader, err := storage.Get(name)
557 if err != nil {
558 return errgo.Annotate(err, "charm not found in the provider stor age")
559 }
560 defer reader.Close()
561 data, err := ioutil.ReadAll(reader)
562 if err != nil {
563 return errgo.Annotate(err, "cannot read charm data")
564 }
565 tempCharm, err := ioutil.TempFile("", "charm")
566 if err != nil {
567 return errgo.Annotate(err, "cannot create charm archive temp fil e")
568 }
569 defer tempCharm.Close()
570 defer os.Remove(tempCharm.Name())
571 if err = ioutil.WriteFile(tempCharm.Name(), data, 0644); err != nil {
572 return errgo.Annotate(err, "error processing charm archive downl oad")
573 }
574
575 // Read and expand the charm bundle.
576 bundle, err := charm.ReadBundle(tempCharm.Name())
577 if err != nil {
578 return errgo.Annotate(err, "cannot read the charm bundle")
579 }
580 // In order to avoid races, the bundle is expanded in a temporary dir wh ich
581 // is then atomically renamed. The temporary directory is created in the
582 // charm cache so that we can safely assume the rename source and target
583 // live in the same file system.
584 cacheDir, _ := filepath.Split(bundlePath)
585 if err = os.MkdirAll(cacheDir, 0755); err != nil {
586 return errgo.Annotate(err, "cannot create the charms cache")
587 }
588 bundleTempPath, err := ioutil.TempDir(cacheDir, "bundle")
589 if err != nil {
590 return errgo.Annotate(err, "cannot create the temporary bundle d irectory")
591 }
592 if err = bundle.ExpandTo(bundleTempPath); err != nil {
593 defer os.RemoveAll(bundleTempPath)
594 return errgo.Annotate(err, "error expanding the bundle")
595 }
596 if err = os.Rename(bundleTempPath, bundlePath); err != nil {
597 return errgo.Annotate(err, "error renaming the bundle")
598 }
599 return nil
600 }
OLDNEW

Powered by Google App Engine
RSS Feeds Recent Issues | This issue
This is Rietveld f62528b