Implement MinIO as cloud storage for RESTAPI agent
[icn.git] / cmd / bpa-restapi-agent / api / imagehandler.go
1 package api
2
3 import (
4         "bytes"
5         "encoding/base64"
6         "encoding/json"
7         "fmt"
8         "io"
9         "io/ioutil"
10         "net/http"
11         "os"
12         "os/user"
13         "log"
14         "path"
15         "strconv"
16
17         image "bpa-restapi-agent/internal/app"
18         minioc "bpa-restapi-agent/internal/storage"
19
20         "github.com/gorilla/mux"
21 )
22
23 // imageHandler is used to store backend implementations objects
24 // Also simplifies mocking for unit testing purposes
25 type imageHandler struct {
26         // Interface that implements Image operations
27         // We will set this variable with a mock interface for testing
28         client image.ImageManager
29         dirPath string
30         minioI minioc.MinIOInfo
31         storeName string  // as minio client bucketname
32 }
33
34 // CreateHandler handles creation of the image entry in the database
35
36 func (h imageHandler) createHandler(w http.ResponseWriter, r *http.Request) {
37         var v image.Image
38
39         // Implemenation using multipart form
40         // Review and enable/remove at a later date
41         // Set Max size to 16mb here
42         err := r.ParseMultipartForm(16777216)
43         if err != nil {
44                 http.Error(w, err.Error(), http.StatusUnprocessableEntity)
45                 return
46         }
47
48         jsn := bytes.NewBuffer([]byte(r.FormValue("metadata")))
49         err = json.NewDecoder(jsn).Decode(&v)
50         switch {
51         case err == io.EOF:
52                 http.Error(w, "Empty body", http.StatusBadRequest)
53                 return
54         case err != nil:
55                 http.Error(w, err.Error(), http.StatusUnprocessableEntity)
56                 return
57         }
58
59         // Name is required.
60         if v.ImageName == "" {
61                 http.Error(w, "Missing name in POST request", http.StatusBadRequest)
62                 return
63         }
64
65         // Owner is required.
66         if v.Owner == "" {
67                 http.Error(w, "Missing Owner in POST request", http.StatusBadRequest)
68                 return
69         }
70
71         if v.ImageLength == 0 {
72                 e := "Improper upload length"
73                 w.WriteHeader(http.StatusBadRequest)
74                 w.Write([]byte(e))
75                 return
76         }
77
78         //Create file directory
79         dir, err := createFileDir(v.Type)
80         if err != nil {
81                 log.Fatal("Error creating file server directory", err)
82         }
83
84         //Read the file section and ignore the header
85         file, _, err := r.FormFile("file")
86         if err != nil {
87                 http.Error(w, "Unable to process file", http.StatusUnprocessableEntity)
88                 return
89         }
90
91         defer file.Close()
92
93         //Convert the file content to base64 for storage
94         content, err := ioutil.ReadAll(file)
95         if err != nil {
96                 http.Error(w, "Unable to read file", http.StatusUnprocessableEntity)
97                 return
98         }
99
100         v.Config = base64.StdEncoding.EncodeToString(content)
101
102         ret, err := h.client.Create(v)
103         if err != nil {
104                 http.Error(w, err.Error(), http.StatusInternalServerError)
105                 return
106         }
107         h.dirPath = dir
108         filePath := path.Join(h.dirPath, v.ImageName)
109         file1, err := os.Create(filePath)
110         if err != nil {
111                 e := "Error creating file in filesystem"
112                 log.Printf("%s %s\n", e, err)
113                 w.WriteHeader(http.StatusInternalServerError)
114                 return
115         }
116
117         defer file1.Close()
118
119         w.Header().Set("Content-Type", "application/json")
120         w.WriteHeader(http.StatusCreated)
121         err = json.NewEncoder(w).Encode(ret)
122         if err != nil {
123                 http.Error(w, err.Error(), http.StatusInternalServerError)
124                 return
125         }
126 }
127
128 // Create file
129
130 func createFileDir(dirName string) (string, error) {
131     u, err := user.Current()
132     if err != nil {
133         log.Println("Error while fetching user home directory", err)
134         return "", err
135     }
136     home := u.HomeDir
137     dirPath := path.Join(home, "images", dirName)
138     err = os.MkdirAll(dirPath, 0744)
139     if err != nil {
140         log.Println("Error while creating file server directory", err)
141         return "", err
142     }
143     return dirPath, nil
144 }
145
146 // getHandler handles GET operations on a particular name
147 // Returns an Image
148 func (h imageHandler) getHandler(w http.ResponseWriter, r *http.Request) {
149         vars := mux.Vars(r)
150         // ownerName := vars["owner"]
151         // clusterName := vars["clustername"]
152         imageName := vars["imgname"]
153
154         ret, err := h.client.Get(imageName)
155         if err != nil {
156                 http.Error(w, err.Error(), http.StatusInternalServerError)
157                 return
158         }
159
160         w.Header().Set("Content-Type", "application/json")
161         w.WriteHeader(http.StatusOK)
162         err = json.NewEncoder(w).Encode(ret)
163         if err != nil {
164                 http.Error(w, err.Error(), http.StatusInternalServerError)
165                 return
166         }
167 }
168
169 // deleteHandler handles DELETE operations on a particular record
170 func (h imageHandler) deleteHandler(w http.ResponseWriter, r *http.Request) {
171         vars := mux.Vars(r)
172         // ownerName := vars["owner"]
173         // clusterName := vars["clustername"]
174         imageName := vars["imgname"]
175
176         err := h.client.Delete(imageName)
177         if err != nil {
178                 http.Error(w, err.Error(), http.StatusInternalServerError)
179                 return
180         }
181
182         h.minioI.DeleteImage(h.storeName, imageName)
183
184         w.WriteHeader(http.StatusNoContent)
185 }
186
187 // UpdateHandler handles Update operations on a particular image
188 func (h imageHandler) updateHandler(w http.ResponseWriter, r *http.Request) {
189         var v image.Image
190         vars := mux.Vars(r)
191         imageName := vars["imgname"]
192
193         err := r.ParseMultipartForm(16777216)
194         if err != nil {
195                 http.Error(w, err.Error(), http.StatusUnprocessableEntity)
196                 return
197         }
198
199         jsn := bytes.NewBuffer([]byte(r.FormValue("metadata")))
200         err = json.NewDecoder(jsn).Decode(&v)
201         switch {
202         case err == io.EOF:
203                 http.Error(w, "Empty body", http.StatusBadRequest)
204                 return
205         case err != nil:
206                 http.Error(w, err.Error(), http.StatusUnprocessableEntity)
207                 return
208         }
209
210         // Name is required.
211         if v.ImageName == "" {
212                 http.Error(w, "Missing name in PUT request", http.StatusBadRequest)
213                 return
214         }
215
216         // Owner is required.
217         if v.Owner == "" {
218                 http.Error(w, "Missing Owner in PUT request", http.StatusBadRequest)
219                 return
220         }
221
222         //Read the file section and ignore the header
223         file, _, err := r.FormFile("file")
224         if err != nil {
225                 http.Error(w, "Unable to process file", http.StatusUnprocessableEntity)
226                 return
227         }
228
229         defer file.Close()
230
231         //Convert the file content to base64 for storage
232         content, err := ioutil.ReadAll(file)
233         if err != nil {
234                 http.Error(w, "Unable to read file", http.StatusUnprocessableEntity)
235                 return
236         }
237
238         v.Config = base64.StdEncoding.EncodeToString(content)
239
240         ret, err := h.client.Update(imageName, v)
241         if err != nil {
242                 http.Error(w, err.Error(), http.StatusInternalServerError)
243                 return
244         }
245
246         w.Header().Set("Content-Type", "application/json")
247         w.WriteHeader(http.StatusCreated)
248         err = json.NewEncoder(w).Encode(ret)
249         if err != nil {
250                 http.Error(w, err.Error(), http.StatusInternalServerError)
251                 return
252         }
253 }
254
255 // File upload is handled by the patchHandler
256
257 func (h imageHandler) patchHandler(w http.ResponseWriter, r *http.Request) {
258         log.Println("going to patch file")
259         vars := mux.Vars(r)
260         imageName := vars["imgname"]
261         file, err := h.client.Get(imageName)
262         if err != nil {
263                 http.Error(w, err.Error(), http.StatusInternalServerError)
264                 return
265         }
266         if *file.UploadComplete == true {
267                 e := "Upload already completed"
268                 w.WriteHeader(http.StatusUnprocessableEntity)
269                 w.Write([]byte(e))
270                 log.Println("Upload already completed")
271                 return
272         }
273         off, err := strconv.Atoi(r.Header.Get("Upload-Offset"))
274         if err != nil {
275                 log.Println("Improper upload offset", err)
276                 w.WriteHeader(http.StatusBadRequest)
277                 return
278         }
279         log.Printf("Upload offset %d\n", off)
280         if *file.ImageOffset != off {
281                 e := fmt.Sprintf("Expected Offset %d, actual offset %d", *file.ImageOffset, off)
282                 w.WriteHeader(http.StatusConflict)
283                 w.Write([]byte(e))
284                 log.Printf("Expected Offset:%d doesn't match got offset:%d\n", *file.ImageOffset, off)
285                 return
286         }
287
288         log.Println("Content length is", r.Header.Get("Content-Length"))
289         clh := r.Header.Get("Content-Length")
290         cl, err := strconv.Atoi(clh)
291         if err != nil {
292                 log.Println("unknown content length")
293                 w.WriteHeader(http.StatusInternalServerError)
294                 return
295         }
296
297         if cl != (file.ImageLength - *file.ImageOffset) {
298                 e := fmt.Sprintf("Content length doesn't match upload length. Expected content length %d got %d", file.ImageLength-*file.ImageOffset, cl)
299                 log.Println(e)
300                 w.WriteHeader(http.StatusBadRequest)
301                 w.Write([]byte(e))
302                 return
303         }
304
305         body, err := ioutil.ReadAll(r.Body)
306         if err != nil {
307                 log.Printf("Received file partially %s\n", err)
308                 log.Println("Size of received file ", len(body))
309         }
310
311         u, err := user.Current()
312         if err != nil {
313                         log.Println("Error while fetching user home directory", err)
314                         return
315         }
316         home := u.HomeDir
317         dir := path.Join(home, "images", file.Type)
318         h.dirPath = dir
319         fp := fmt.Sprintf("%s/%s", h.dirPath, imageName)
320         f, err := os.OpenFile(fp, os.O_APPEND|os.O_WRONLY, 0644)
321         if err != nil {
322                 log.Printf("unable to open file %s\n", err)
323                 w.WriteHeader(http.StatusInternalServerError)
324                 return
325         }
326         defer f.Close()
327
328         n, err := f.WriteAt(body, int64(off))
329         if err != nil {
330                 log.Printf("unable to write %s", err)
331                 w.WriteHeader(http.StatusInternalServerError)
332                 return
333         }
334
335         log.Printf("Start to Patch image, bucket: %s, image: %s, dirpath: %s, offset: %d, n: %d\n",
336                 h.storeName, imageName, fp, *file.ImageOffset, n)
337         uploadbytes, err := h.minioI.PatchImage(h.storeName, imageName, fp, int64(*file.ImageOffset), int64(n))
338         if err != nil || uploadbytes == 0  {
339                 log.Printf("MinIO upload with offset %d failed: %s", *file.ImageOffset, err)
340                 w.WriteHeader(http.StatusInternalServerError)
341                 return
342     }
343
344         log.Println("number of bytes written ", n)
345         no := *file.ImageOffset + n
346         file.ImageOffset = &no
347
348         uo := strconv.Itoa(*file.ImageOffset)
349         w.Header().Set("Upload-Offset", uo)
350         if *file.ImageOffset == file.ImageLength {
351                 log.Println("upload completed successfully")
352                 *file.UploadComplete = true
353         }
354
355         // err = h.updateFile(file)
356         // if err != nil {
357         //      log.Println("Error while updating file", err)
358         //      w.WriteHeader(http.StatusInternalServerError)
359         //      return
360         // }
361         w.WriteHeader(http.StatusNoContent)
362
363         return
364
365 }