-
Notifications
You must be signed in to change notification settings - Fork 0
/
backup.go
135 lines (114 loc) · 3.58 KB
/
backup.go
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
// Copyright 2014 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.
package apiserver
import (
"encoding/json"
"io"
"net/http"
"github.com/juju/errors"
apiservererrors "github.com/juju/juju/apiserver/errors"
"github.com/juju/juju/rpc/params"
"github.com/juju/juju/state/backups"
)
var newBackups = backups.NewBackups
// backupHandler handles backup requests.
type backupHandler struct {
ctxt httpContext
}
func (h *backupHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
// Validate before authenticate because the authentication is dependent
// on the state connection that is determined during the validation.
st, err := h.ctxt.stateForRequestAuthenticatedUser(req)
if err != nil {
h.sendError(resp, err)
return
}
defer st.Release()
if !st.IsController() {
h.sendError(resp, errors.New("requested model is not the controller model"))
return
}
switch req.Method {
case "GET":
logger.Infof("handling backups download request")
model, err := st.Model()
if err != nil {
h.sendError(resp, err)
return
}
modelConfig, err := model.ModelConfig()
if err != nil {
h.sendError(resp, err)
return
}
backupDir := backups.BackupDirToUse(modelConfig.BackupDir())
paths := &backups.Paths{
BackupDir: backupDir,
}
id, err := h.download(newBackups(paths), resp, req)
if err != nil {
h.sendError(resp, err)
return
}
logger.Infof("backups download request successful for %q", id)
default:
h.sendError(resp, errors.MethodNotAllowedf("unsupported method: %q", req.Method))
}
}
func (h *backupHandler) download(backups backups.Backups, resp http.ResponseWriter, req *http.Request) (string, error) {
args, err := h.parseGETArgs(req)
if err != nil {
return "", err
}
logger.Infof("backups download request for %q", args.ID)
meta, archive, err := backups.Get(args.ID)
if err != nil {
return "", err
}
defer archive.Close()
err = h.sendFile(archive, meta.Checksum(), resp)
return args.ID, err
}
func (h *backupHandler) read(req *http.Request, expectedType string) ([]byte, error) {
defer req.Body.Close()
ctype := req.Header.Get("Content-Type")
if ctype != expectedType {
return nil, errors.Errorf("expected Content-Type %q, got %q", expectedType, ctype)
}
body, err := io.ReadAll(req.Body)
if err != nil {
return nil, errors.Annotate(err, "while reading request body")
}
return body, nil
}
func (h *backupHandler) parseGETArgs(req *http.Request) (*params.BackupsDownloadArgs, error) {
body, err := h.read(req, params.ContentTypeJSON)
if err != nil {
return nil, errors.Trace(err)
}
var args params.BackupsDownloadArgs
if err := json.Unmarshal(body, &args); err != nil {
return nil, errors.Annotate(err, "while de-serializing args")
}
return &args, nil
}
func (h *backupHandler) sendFile(file io.Reader, checksum string, resp http.ResponseWriter) error {
// We don't set the Content-Length header, leaving it at -1.
resp.Header().Set("Content-Type", params.ContentTypeRaw)
resp.Header().Set("Digest", params.EncodeChecksum(checksum))
resp.WriteHeader(http.StatusOK)
if _, err := io.Copy(resp, file); err != nil {
return errors.Annotate(err, "while streaming archive")
}
return nil
}
// sendError sends a JSON-encoded error response.
// Note the difference from the error response sent by
// the sendError function - the error is encoded directly
// rather than in the Error field.
func (h *backupHandler) sendError(w http.ResponseWriter, err error) {
err, status := apiservererrors.ServerErrorAndStatus(err)
if err := sendStatusAndJSON(w, status, err); err != nil {
logger.Errorf("%v", err)
}
}