blob: 5c3226e405ee3aa9d89c1e596daceabd87d6514e [file] [log] [blame]
mpagenkoc26d4c02021-05-06 14:27:57 +00001/*
2 * Copyright 2020-present Open Networking Foundation
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17//Package adaptercoreonu provides the utility for onu devices, flows and statistics
18package adaptercoreonu
19
20import (
21 "bufio"
22 "context"
23 "fmt"
24 "io"
25 "net/http"
26 "net/url"
27 "os"
Andrey Pozolotin1394a1c2021-06-01 00:54:18 +030028 "path/filepath"
mpagenkoc26d4c02021-05-06 14:27:57 +000029 "sync"
30 "time"
31
khenaidoo7d3c5582021-08-11 18:09:44 -040032 "github.com/opencord/voltha-lib-go/v7/pkg/log"
mpagenkoc26d4c02021-05-06 14:27:57 +000033)
34
35const cDefaultLocalDir = "/tmp" //this is the default local dir to download to
36
37type fileState uint32
38
39//nolint:varcheck, deadcode
40const (
41 cFileStateUnknown fileState = iota
42 cFileStateDlStarted
43 cFileStateDlSucceeded
44 cFileStateDlFailed
45 cFileStateDlAborted
46 cFileStateDlInvalid
47)
48
49type downloadImageParams struct {
mpagenko39b703e2021-08-25 13:38:40 +000050 downloadImageName string
51 downloadImageState fileState
52 downloadImageLen int64
53 downloadImageCrc uint32
54 downloadActive bool
55 downloadContextCancelFn context.CancelFunc
mpagenkoc26d4c02021-05-06 14:27:57 +000056}
57
58type requesterChannelMap map[chan<- bool]struct{} //using an empty structure map for easier (unique) element appending
59
60//fileDownloadManager structure holds information needed for downloading to and storing images within the adapter
61type fileDownloadManager struct {
62 mutexDownloadImageDsc sync.RWMutex
63 downloadImageDscSlice []downloadImageParams
64 dnldImgReadyWaiting map[string]requesterChannelMap
65 dlToAdapterTimeout time.Duration
66}
67
68//newFileDownloadManager constructor returns a new instance of a fileDownloadManager
69//mib_db (as well as not inluded alarm_db not really used in this code? VERIFY!!)
70func newFileDownloadManager(ctx context.Context) *fileDownloadManager {
71 logger.Debug(ctx, "init-fileDownloadManager")
72 var localDnldMgr fileDownloadManager
73 localDnldMgr.downloadImageDscSlice = make([]downloadImageParams, 0)
74 localDnldMgr.dnldImgReadyWaiting = make(map[string]requesterChannelMap)
75 localDnldMgr.dlToAdapterTimeout = 10 * time.Second //default timeout, should be overwritten immediately after start
76 return &localDnldMgr
77}
78
79//SetDownloadTimeout configures the timeout used to supervice the download of the image to the adapter (assumed in seconds)
80func (dm *fileDownloadManager) SetDownloadTimeout(ctx context.Context, aDlTimeout time.Duration) {
81 dm.mutexDownloadImageDsc.Lock()
82 defer dm.mutexDownloadImageDsc.Unlock()
83 logger.Debugw(ctx, "setting download timeout", log.Fields{"timeout": aDlTimeout})
84 dm.dlToAdapterTimeout = aDlTimeout
85}
86
87//GetDownloadTimeout delivers the timeout used to supervice the download of the image to the adapter (assumed in seconds)
88func (dm *fileDownloadManager) GetDownloadTimeout(ctx context.Context) time.Duration {
89 dm.mutexDownloadImageDsc.RLock()
90 defer dm.mutexDownloadImageDsc.RUnlock()
91 return dm.dlToAdapterTimeout
92}
93
94//ImageExists returns true if the requested image already exists within the adapter
95func (dm *fileDownloadManager) ImageExists(ctx context.Context, aImageName string) bool {
96 logger.Debugw(ctx, "checking on existence of the image", log.Fields{"image-name": aImageName})
97 dm.mutexDownloadImageDsc.RLock()
98 defer dm.mutexDownloadImageDsc.RUnlock()
99
100 for _, dnldImgDsc := range dm.downloadImageDscSlice {
101 if dnldImgDsc.downloadImageName == aImageName {
102 //image found (by name)
103 return true
104 }
105 }
106 //image not found (by name)
107 return false
108}
109
110//StartDownload returns true if the download of the requested image could be started for the given file name and URL
111func (dm *fileDownloadManager) StartDownload(ctx context.Context, aImageName string, aURLCommand string) error {
112 logger.Infow(ctx, "image download-to-adapter requested", log.Fields{
113 "image-name": aImageName, "url-command": aURLCommand})
114 loDownloadImageParams := downloadImageParams{
115 downloadImageName: aImageName, downloadImageState: cFileStateDlStarted,
116 downloadImageLen: 0, downloadImageCrc: 0}
mpagenkoc26d4c02021-05-06 14:27:57 +0000117 //try to download from http
mpagenko38662d02021-08-11 09:45:19 +0000118 var err error
119 if err = dm.downloadFile(ctx, aURLCommand, cDefaultLocalDir, aImageName); err == nil {
120 dm.mutexDownloadImageDsc.Lock()
121 dm.downloadImageDscSlice = append(dm.downloadImageDscSlice, loDownloadImageParams)
122 dm.mutexDownloadImageDsc.Unlock()
123 }
mpagenkoc26d4c02021-05-06 14:27:57 +0000124 //return the result of the start-request to comfort the core processing even though the complete download may go on in background
125 return err
126}
127
128//GetImageBufferLen returns the length of the specified file in bytes (file size) - as detected after download
129func (dm *fileDownloadManager) GetImageBufferLen(ctx context.Context, aFileName string) (int64, error) {
130 dm.mutexDownloadImageDsc.RLock()
131 defer dm.mutexDownloadImageDsc.RUnlock()
132 for _, dnldImgDsc := range dm.downloadImageDscSlice {
133 if dnldImgDsc.downloadImageName == aFileName && dnldImgDsc.downloadImageState == cFileStateDlSucceeded {
134 //image found (by name) and fully downloaded
135 return dnldImgDsc.downloadImageLen, nil
136 }
137 }
138 return 0, fmt.Errorf("no downloaded image found: %s", aFileName)
139}
140
141//GetDownloadImageBuffer returns the content of the requested file as byte slice
142func (dm *fileDownloadManager) GetDownloadImageBuffer(ctx context.Context, aFileName string) ([]byte, error) {
Andrey Pozolotin1394a1c2021-06-01 00:54:18 +0300143 file, err := os.Open(filepath.Clean(cDefaultLocalDir + "/" + aFileName))
mpagenkoc26d4c02021-05-06 14:27:57 +0000144 if err != nil {
145 return nil, err
146 }
Andrey Pozolotin1394a1c2021-06-01 00:54:18 +0300147 defer func() {
148 err := file.Close()
149 if err != nil {
150 logger.Errorw(ctx, "failed to close file", log.Fields{"error": err})
151 }
152 }()
mpagenkoc26d4c02021-05-06 14:27:57 +0000153
154 stats, statsErr := file.Stat()
155 if statsErr != nil {
156 return nil, statsErr
157 }
158
159 var size int64 = stats.Size()
160 bytes := make([]byte, size)
161
162 buffer := bufio.NewReader(file)
163 _, err = buffer.Read(bytes)
164
165 return bytes, err
166}
167
168//RequestDownloadReady receives a channel that has to be used to inform the requester in case the concerned file is downloaded
169func (dm *fileDownloadManager) RequestDownloadReady(ctx context.Context, aFileName string, aWaitChannel chan<- bool) {
mpagenko5b5cb982021-08-18 16:38:51 +0000170 //mutexDownloadImageDsc must already be locked here to avoid an update of the dnldImgReadyWaiting map
171 // just after returning false on imageLocallyDownloaded() (not found) and immediate handling of the
172 // download success (within updateFileState())
173 // so updateFileState() can't interfere here just after imageLocallyDownloaded() before setting the requester map
174 dm.mutexDownloadImageDsc.Lock()
175 defer dm.mutexDownloadImageDsc.Unlock()
mpagenkoc26d4c02021-05-06 14:27:57 +0000176 if dm.imageLocallyDownloaded(ctx, aFileName) {
177 //image found (by name) and fully downloaded
178 logger.Debugw(ctx, "file ready - immediate response", log.Fields{"image-name": aFileName})
179 aWaitChannel <- true
180 return
181 }
182 //when we are here the image was not yet found or not fully downloaded -
183 // add the device specific channel to the list of waiting requesters
mpagenkoc26d4c02021-05-06 14:27:57 +0000184 if loRequesterChannelMap, ok := dm.dnldImgReadyWaiting[aFileName]; ok {
185 //entry for the file name already exists
186 if _, exists := loRequesterChannelMap[aWaitChannel]; !exists {
187 // requester channel does not yet exist for the image
188 loRequesterChannelMap[aWaitChannel] = struct{}{}
189 dm.dnldImgReadyWaiting[aFileName] = loRequesterChannelMap
190 logger.Debugw(ctx, "file not ready - adding new requester", log.Fields{
191 "image-name": aFileName, "number-of-requesters": len(dm.dnldImgReadyWaiting[aFileName])})
192 }
193 } else {
194 //entry for the file name does not even exist
195 addRequesterChannelMap := make(map[chan<- bool]struct{})
196 addRequesterChannelMap[aWaitChannel] = struct{}{}
197 dm.dnldImgReadyWaiting[aFileName] = addRequesterChannelMap
198 logger.Debugw(ctx, "file not ready - setting first requester", log.Fields{
199 "image-name": aFileName})
200 }
201}
202
203//RemoveReadyRequest removes the specified channel from the requester(channel) map for the given file name
204func (dm *fileDownloadManager) RemoveReadyRequest(ctx context.Context, aFileName string, aWaitChannel chan bool) {
205 dm.mutexDownloadImageDsc.Lock()
206 defer dm.mutexDownloadImageDsc.Unlock()
207 for imageName, channelMap := range dm.dnldImgReadyWaiting {
208 if imageName == aFileName {
209 for channel := range channelMap {
210 if channel == aWaitChannel {
211 delete(dm.dnldImgReadyWaiting[imageName], channel)
212 logger.Debugw(ctx, "channel removed from the requester map", log.Fields{
213 "image-name": aFileName, "new number-of-requesters": len(dm.dnldImgReadyWaiting[aFileName])})
214 return //can leave directly
215 }
216 }
217 return //can leave directly
218 }
219 }
220}
221
222// FileDownloadManager private (unexported) methods -- start
223
224//imageLocallyDownloaded returns true if the requested image already exists within the adapter
mpagenko5b5cb982021-08-18 16:38:51 +0000225// requires mutexDownloadImageDsc to be locked (at least RLocked)
mpagenkoc26d4c02021-05-06 14:27:57 +0000226func (dm *fileDownloadManager) imageLocallyDownloaded(ctx context.Context, aImageName string) bool {
227 logger.Debugw(ctx, "checking if image is fully downloaded to adapter", log.Fields{"image-name": aImageName})
mpagenkoc26d4c02021-05-06 14:27:57 +0000228 for _, dnldImgDsc := range dm.downloadImageDscSlice {
229 if dnldImgDsc.downloadImageName == aImageName {
230 //image found (by name)
231 if dnldImgDsc.downloadImageState == cFileStateDlSucceeded {
232 logger.Debugw(ctx, "image has been fully downloaded", log.Fields{"image-name": aImageName})
233 return true
234 }
235 logger.Debugw(ctx, "image not yet fully downloaded", log.Fields{"image-name": aImageName})
236 return false
237 }
238 }
239 //image not found (by name)
240 logger.Errorw(ctx, "image does not exist", log.Fields{"image-name": aImageName})
241 return false
242}
243
mpagenko39b703e2021-08-25 13:38:40 +0000244//updateDownloadCancel sets context cancel function to be used in case the download is to be aborted
245func (dm *fileDownloadManager) updateDownloadCancel(ctx context.Context,
246 aImageName string, aCancelFn context.CancelFunc) {
247 dm.mutexDownloadImageDsc.Lock()
248 defer dm.mutexDownloadImageDsc.Unlock()
249 for imgKey, dnldImgDsc := range dm.downloadImageDscSlice {
250 if dnldImgDsc.downloadImageName == aImageName {
251 //image found (by name) - need to write changes on the original map
252 dm.downloadImageDscSlice[imgKey].downloadContextCancelFn = aCancelFn
253 dm.downloadImageDscSlice[imgKey].downloadActive = true
254 logger.Debugw(ctx, "downloadContextCancelFn set", log.Fields{
255 "image-name": aImageName})
256 return //can leave directly
257 }
258 }
259}
260
mpagenko5b5cb982021-08-18 16:38:51 +0000261//updateFileState sets the new active (downloaded) file state and informs possibly waiting requesters on this change
262func (dm *fileDownloadManager) updateFileState(ctx context.Context, aImageName string, aFileSize int64) {
263 dm.mutexDownloadImageDsc.Lock()
264 defer dm.mutexDownloadImageDsc.Unlock()
265 for imgKey, dnldImgDsc := range dm.downloadImageDscSlice {
266 if dnldImgDsc.downloadImageName == aImageName {
267 //image found (by name) - need to write changes on the original map
mpagenko39b703e2021-08-25 13:38:40 +0000268 dm.downloadImageDscSlice[imgKey].downloadActive = false
mpagenko5b5cb982021-08-18 16:38:51 +0000269 dm.downloadImageDscSlice[imgKey].downloadImageState = cFileStateDlSucceeded
270 dm.downloadImageDscSlice[imgKey].downloadImageLen = aFileSize
271 logger.Debugw(ctx, "imageState download succeeded", log.Fields{
272 "image-name": aImageName, "image-size": aFileSize})
273 //in case upgrade process(es) was/were waiting for the file, inform them
274 for imageName, channelMap := range dm.dnldImgReadyWaiting {
275 if imageName == aImageName {
276 for channel := range channelMap {
277 // use all found channels to inform possible requesters about the existence of the file
278 channel <- true
279 delete(dm.dnldImgReadyWaiting[imageName], channel) //requester served
280 }
281 return //can leave directly
282 }
283 }
284 return //can leave directly
285 }
286 }
287}
288
mpagenkoc26d4c02021-05-06 14:27:57 +0000289//downloadFile downloads the specified file from the given http location
290func (dm *fileDownloadManager) downloadFile(ctx context.Context, aURLCommand string, aFilePath string, aFileName string) error {
291 // Get the data
292 logger.Infow(ctx, "downloading with URL", log.Fields{"url": aURLCommand, "localPath": aFilePath})
293 // verifying the complete URL by parsing it to its URL elements
294 urlBase, err1 := url.Parse(aURLCommand)
295 if err1 != nil {
296 logger.Errorw(ctx, "could not set base url command", log.Fields{"url": aURLCommand, "error": err1})
297 return fmt.Errorf("could not set base url command: %s, error: %s", aURLCommand, err1)
298 }
299 urlParams := url.Values{}
300 urlBase.RawQuery = urlParams.Encode()
301
302 //pre-check on file existence - assuming http location here
303 reqExist, errExist2 := http.NewRequest("HEAD", urlBase.String(), nil)
304 if errExist2 != nil {
305 logger.Errorw(ctx, "could not generate http head request", log.Fields{"url": urlBase.String(), "error": errExist2})
306 return fmt.Errorf("could not generate http head request: %s, error: %s", aURLCommand, errExist2)
307 }
308 ctxExist, cancelExist := context.WithDeadline(ctx, time.Now().Add(3*time.Second)) //waiting for some fast answer
309 defer cancelExist()
310 _ = reqExist.WithContext(ctxExist)
311 respExist, errExist3 := http.DefaultClient.Do(reqExist)
312 if errExist3 != nil || respExist.StatusCode != http.StatusOK {
mpagenko39b703e2021-08-25 13:38:40 +0000313 if respExist == nil {
314 logger.Errorw(ctx, "http head from url error - no status, aborting", log.Fields{"url": urlBase.String(),
315 "error": errExist3})
316 return fmt.Errorf("http head from url error - no status, aborting: %s, error: %s",
317 aURLCommand, errExist3)
318 }
mpagenkoc26d4c02021-05-06 14:27:57 +0000319 logger.Infow(ctx, "could not http head from url", log.Fields{"url": urlBase.String(),
320 "error": errExist3, "status": respExist.StatusCode})
321 //if head is not supported by server we cannot use this test and just try to continue
322 if respExist.StatusCode != http.StatusMethodNotAllowed {
323 logger.Errorw(ctx, "http head from url: file does not exist here, aborting", log.Fields{"url": urlBase.String(),
324 "error": errExist3, "status": respExist.StatusCode})
325 return fmt.Errorf("http head from url: file does not exist here, aborting: %s, error: %s, status: %d",
mpagenko39b703e2021-08-25 13:38:40 +0000326 aURLCommand, errExist3, respExist.StatusCode)
mpagenkoc26d4c02021-05-06 14:27:57 +0000327 }
328 }
329 defer func() {
330 deferredErr := respExist.Body.Close()
331 if deferredErr != nil {
332 logger.Errorw(ctx, "error at closing http head response body", log.Fields{"url": urlBase.String(), "error": deferredErr})
333 }
334 }()
335
336 //trying to download - do it in background as it may take some time ...
337 go func() {
338 req, err2 := http.NewRequest("GET", urlBase.String(), nil)
339 if err2 != nil {
340 logger.Errorw(ctx, "could not generate http request", log.Fields{"url": urlBase.String(), "error": err2})
mpagenko38662d02021-08-11 09:45:19 +0000341 dm.removeImage(ctx, aFileName, false) //wo FileSystem access
mpagenkoc26d4c02021-05-06 14:27:57 +0000342 return
343 }
344 ctx, cancel := context.WithDeadline(ctx, time.Now().Add(dm.dlToAdapterTimeout)) //timeout as given from SetDownloadTimeout()
mpagenko39b703e2021-08-25 13:38:40 +0000345 dm.updateDownloadCancel(ctx, aFileName, cancel)
mpagenkoc26d4c02021-05-06 14:27:57 +0000346 defer cancel()
347 _ = req.WithContext(ctx)
348 resp, err3 := http.DefaultClient.Do(req)
mpagenko39b703e2021-08-25 13:38:40 +0000349 if err3 != nil || resp.StatusCode != http.StatusOK {
350 if resp == nil {
351 logger.Errorw(ctx, "http get error - no status, aborting", log.Fields{"url": urlBase.String(),
352 "error": err3})
353 } else {
354 logger.Errorw(ctx, "could not http get from url", log.Fields{"url": urlBase.String(),
355 "error": err3, "status": resp.StatusCode})
356 }
mpagenko38662d02021-08-11 09:45:19 +0000357 dm.removeImage(ctx, aFileName, false) //wo FileSystem access
mpagenkoc26d4c02021-05-06 14:27:57 +0000358 return
359 }
360 defer func() {
361 deferredErr := resp.Body.Close()
362 if deferredErr != nil {
363 logger.Errorw(ctx, "error at closing http get response body", log.Fields{"url": urlBase.String(), "error": deferredErr})
364 }
365 }()
366
367 // Create the file
368 aLocalPathName := aFilePath + "/" + aFileName
369 file, err := os.Create(aLocalPathName)
370 if err != nil {
371 logger.Errorw(ctx, "could not create local file", log.Fields{"path_file": aLocalPathName, "error": err})
mpagenko38662d02021-08-11 09:45:19 +0000372 dm.removeImage(ctx, aFileName, false) //wo FileSystem access
mpagenkoc26d4c02021-05-06 14:27:57 +0000373 return
374 }
375 defer func() {
376 deferredErr := file.Close()
377 if deferredErr != nil {
378 logger.Errorw(ctx, "error at closing new file", log.Fields{"path_file": aLocalPathName, "error": deferredErr})
379 }
380 }()
381
382 // Write the body to file
383 _, err = io.Copy(file, resp.Body)
384 if err != nil {
385 logger.Errorw(ctx, "could not copy file content", log.Fields{"url": urlBase.String(), "file": aLocalPathName, "error": err})
mpagenko38662d02021-08-11 09:45:19 +0000386 dm.removeImage(ctx, aFileName, true)
mpagenkoc26d4c02021-05-06 14:27:57 +0000387 return
388 }
389
390 fileStats, statsErr := file.Stat()
391 if err != nil {
392 logger.Errorw(ctx, "created file can't be accessed", log.Fields{"file": aLocalPathName, "stat-error": statsErr})
mpagenko5b5cb982021-08-18 16:38:51 +0000393 return
mpagenkoc26d4c02021-05-06 14:27:57 +0000394 }
395 fileSize := fileStats.Size()
396 logger.Infow(ctx, "written file size is", log.Fields{"file": aLocalPathName, "length": fileSize})
397
mpagenko5b5cb982021-08-18 16:38:51 +0000398 dm.updateFileState(ctx, aFileName, fileSize)
mpagenkoc26d4c02021-05-06 14:27:57 +0000399 //TODO:!!! further extension could be provided here, e.g. already computing and possibly comparing the CRC, vendor check
400 }()
401 return nil
402}
mpagenkoaa3afe92021-05-21 16:20:58 +0000403
mpagenko38662d02021-08-11 09:45:19 +0000404//removeImage deletes the given image according to the Image name from filesystem and downloadImageDscSlice
405func (dm *fileDownloadManager) removeImage(ctx context.Context, aImageName string, aDelFs bool) {
406 logger.Debugw(ctx, "remove the image from Adapter", log.Fields{"image-name": aImageName})
mpagenkoaa3afe92021-05-21 16:20:58 +0000407 dm.mutexDownloadImageDsc.RLock()
408 defer dm.mutexDownloadImageDsc.RUnlock()
409
410 tmpSlice := dm.downloadImageDscSlice[:0]
411 for _, dnldImgDsc := range dm.downloadImageDscSlice {
412 if dnldImgDsc.downloadImageName == aImageName {
mpagenko38662d02021-08-11 09:45:19 +0000413 //image found (by name)
mpagenkoaa3afe92021-05-21 16:20:58 +0000414 logger.Debugw(ctx, "removing image", log.Fields{"image-name": aImageName})
mpagenko38662d02021-08-11 09:45:19 +0000415 if aDelFs {
416 //remove the image from filesystem
417 aLocalPathName := cDefaultLocalDir + "/" + aImageName
418 if err := os.Remove(aLocalPathName); err != nil {
419 // might be a temporary situation, when the file was not yet (completely) written
420 logger.Debugw(ctx, "image not removed from filesystem", log.Fields{
421 "image-name": aImageName, "error": err})
422 }
mpagenkoaa3afe92021-05-21 16:20:58 +0000423 }
mpagenko38662d02021-08-11 09:45:19 +0000424 // and remove from the imageDsc slice by just not appending
mpagenkoaa3afe92021-05-21 16:20:58 +0000425 } else {
426 tmpSlice = append(tmpSlice, dnldImgDsc)
427 }
428 }
429 dm.downloadImageDscSlice = tmpSlice
430 //image not found (by name)
431}
mpagenko38662d02021-08-11 09:45:19 +0000432
mpagenko39b703e2021-08-25 13:38:40 +0000433//CancelDownload stops the download and clears all entires concerning this aimageName
mpagenko38662d02021-08-11 09:45:19 +0000434func (dm *fileDownloadManager) CancelDownload(ctx context.Context, aImageName string) {
435 // for the moment that would only support to wait for the download end and remove the image then
436 // further reactions while still downloading can be considered with some effort, but does it make sense (synchronous load here!)
mpagenko39b703e2021-08-25 13:38:40 +0000437 dm.mutexDownloadImageDsc.RLock()
438 for imgKey, dnldImgDsc := range dm.downloadImageDscSlice {
439 if dnldImgDsc.downloadImageName == aImageName {
440 //image found (by name) - need to to check on ongoing download
441 if dm.downloadImageDscSlice[imgKey].downloadActive {
442 //then cancel the download using the context cancel function
443 dm.downloadImageDscSlice[imgKey].downloadContextCancelFn()
444 }
445 //and remove possibly stored traces of this image
446 dm.mutexDownloadImageDsc.RUnlock()
447 go dm.removeImage(ctx, aImageName, true) //including the chance that nothing was yet written to FS, should not matter
448 return //can leave directly
449 }
450 }
451 dm.mutexDownloadImageDsc.RUnlock()
mpagenko38662d02021-08-11 09:45:19 +0000452}