jottacloud: refactor timestamp handling

Jottacloud have several different apis and endpoints using a mix of different
timestamp formats. In existing code the list operation (after the recent liststream
implementation) uses format from golang's time.RFC3339 constant. Uploads (using the
allocate api) uses format from a hard coded constant with value identical to golang's
time.RFC3339. And then we have the classic JFS time format, which is similar to RFC3339
but not identical, using a different constant. Also the naming is a bit confusing,
since the term api is used both as a generic term and also as a reference to the
newer format used in the api subdomain where the allocate endpoint is located.
This commit refactors these things a bit.
This commit is contained in:
albertony 2022-04-24 19:34:29 +02:00
parent cc8dde402f
commit 4829527dac
2 changed files with 68 additions and 60 deletions

View File

@ -8,42 +8,69 @@ import (
) )
const ( const (
// default time format for almost all request and responses // default time format historically used for all request and responses.
timeFormat = "2006-01-02-T15:04:05Z0700" // Similar to time.RFC3339, but with an extra '-' in front of 'T',
// the API server seems to use a different format // and no ':' separator in timezone offset. Some newer endpoints have
apiTimeFormat = "2006-01-02T15:04:05Z07:00" // moved to proper time.RFC3339 conformant format instead.
jottaTimeFormat = "2006-01-02-T15:04:05Z0700"
) )
// Time represents time values in the Jottacloud API. It uses a custom RFC3339 like format. // unmarshalXML turns XML into a Time
type Time time.Time func unmarshalXMLTime(d *xml.Decoder, start xml.StartElement, timeFormat string) (time.Time, error) {
// UnmarshalXML turns XML into a Time
func (t *Time) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var v string var v string
if err := d.DecodeElement(&v, &start); err != nil { if err := d.DecodeElement(&v, &start); err != nil {
return err return time.Time{}, err
} }
if v == "" { if v == "" {
*t = Time(time.Time{}) return time.Time{}, nil
return nil
} }
newTime, err := time.Parse(timeFormat, v) newTime, err := time.Parse(timeFormat, v)
if err == nil { if err == nil {
*t = Time(newTime) return newTime, nil
} }
return time.Time{}, err
}
// JottaTime represents time values in the classic API using a custom RFC3339 like format
type JottaTime time.Time
// String returns JottaTime string in Jottacloud classic format
func (t JottaTime) String() string { return time.Time(t).Format(jottaTimeFormat) }
// UnmarshalXML turns XML into a JottaTime
func (t *JottaTime) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
tm, err := unmarshalXMLTime(d, start, jottaTimeFormat)
*t = JottaTime(tm)
return err return err
} }
// MarshalXML turns a Time into XML // MarshalXML turns a JottaTime into XML
func (t *Time) MarshalXML(e *xml.Encoder, start xml.StartElement) error { func (t *JottaTime) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return e.EncodeElement(t.String(), start) return e.EncodeElement(t.String(), start)
} }
// Return Time string in Jottacloud format // Rfc3339Time represents time values in the newer APIs using standard RFC3339 format
func (t Time) String() string { return time.Time(t).Format(timeFormat) } type Rfc3339Time time.Time
// APIString returns Time string in Jottacloud API format // String returns Rfc3339Time string in Jottacloud RFC3339 format
func (t Time) APIString() string { return time.Time(t).Format(apiTimeFormat) } func (t Rfc3339Time) String() string { return time.Time(t).Format(time.RFC3339) }
// UnmarshalXML turns XML into a Rfc3339Time
func (t *Rfc3339Time) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
tm, err := unmarshalXMLTime(d, start, time.RFC3339)
*t = Rfc3339Time(tm)
return err
}
// MarshalXML turns a Rfc3339Time into XML
func (t *Rfc3339Time) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return e.EncodeElement(t.String(), start)
}
// MarshalJSON turns a Rfc3339Time into JSON
func (t *Rfc3339Time) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf("\"%s\"", t.String())), nil
}
// LoginToken is struct representing the login token generated in the WebUI // LoginToken is struct representing the login token generated in the WebUI
type LoginToken struct { type LoginToken struct {
@ -333,9 +360,9 @@ type JottaFolder struct {
Name string `xml:"name,attr"` Name string `xml:"name,attr"`
Deleted Flag `xml:"deleted,attr"` Deleted Flag `xml:"deleted,attr"`
Path string `xml:"path"` Path string `xml:"path"`
CreatedAt Time `xml:"created"` CreatedAt JottaTime `xml:"created"`
ModifiedAt Time `xml:"modified"` ModifiedAt JottaTime `xml:"modified"`
Updated Time `xml:"updated"` Updated JottaTime `xml:"updated"`
Folders []JottaFolder `xml:"folders>folder"` Folders []JottaFolder `xml:"folders>folder"`
Files []JottaFile `xml:"files>file"` Files []JottaFile `xml:"files>file"`
} }
@ -365,9 +392,9 @@ type JottaFile struct {
PublicURI string `xml:"publicURI"` PublicURI string `xml:"publicURI"`
PublicSharePath string `xml:"publicSharePath"` PublicSharePath string `xml:"publicSharePath"`
State string `xml:"currentRevision>state"` State string `xml:"currentRevision>state"`
CreatedAt Time `xml:"currentRevision>created"` CreatedAt JottaTime `xml:"currentRevision>created"`
ModifiedAt Time `xml:"currentRevision>modified"` ModifiedAt JottaTime `xml:"currentRevision>modified"`
Updated Time `xml:"currentRevision>updated"` Updated JottaTime `xml:"currentRevision>updated"`
Size int64 `xml:"currentRevision>size"` Size int64 `xml:"currentRevision>size"`
MimeType string `xml:"currentRevision>mime"` MimeType string `xml:"currentRevision>mime"`
MD5 string `xml:"currentRevision>md5"` MD5 string `xml:"currentRevision>md5"`

View File

@ -932,25 +932,6 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
return entries, nil return entries, nil
} }
type listStreamTime time.Time
func (c *listStreamTime) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var v string
if err := d.DecodeElement(&v, &start); err != nil {
return err
}
t, err := time.Parse(time.RFC3339, v)
if err != nil {
return err
}
*c = listStreamTime(t)
return nil
}
func (c listStreamTime) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf("\"%s\"", time.Time(c).Format(time.RFC3339))), nil
}
func parseListRStream(ctx context.Context, r io.Reader, trimPrefix string, filesystem *Fs, callback func(fs.DirEntry) error) error { func parseListRStream(ctx context.Context, r io.Reader, trimPrefix string, filesystem *Fs, callback func(fs.DirEntry) error) error {
type stats struct { type stats struct {
@ -964,8 +945,8 @@ func parseListRStream(ctx context.Context, r io.Reader, trimPrefix string, files
Name string `xml:"filename"` Name string `xml:"filename"`
Checksum string `xml:"md5"` Checksum string `xml:"md5"`
Size int64 `xml:"size"` Size int64 `xml:"size"`
Modified listStreamTime `xml:"modified"` Modified api.Rfc3339Time `xml:"modified"` // Note: Liststream response includes 3 decimal milliseconds, but we ignore them since there is second precision everywhere else
Created listStreamTime `xml:"created"` Created api.Rfc3339Time `xml:"created"`
} }
type xmlFolder struct { type xmlFolder struct {
@ -1228,7 +1209,7 @@ func (f *Fs) createOrUpdate(ctx context.Context, file string, modTime time.Time,
opts.Parameters.Set("cphash", "true") opts.Parameters.Set("cphash", "true")
fileDate := api.Time(modTime).String() fileDate := api.JottaTime(modTime).String()
opts.ExtraHeaders["JSize"] = strconv.FormatInt(size, 10) opts.ExtraHeaders["JSize"] = strconv.FormatInt(size, 10)
opts.ExtraHeaders["JMd5"] = md5 opts.ExtraHeaders["JMd5"] = md5
opts.ExtraHeaders["JCreated"] = fileDate opts.ExtraHeaders["JCreated"] = fileDate
@ -1749,7 +1730,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
Options: options, Options: options,
ExtraHeaders: make(map[string]string), ExtraHeaders: make(map[string]string),
} }
fileDate := api.Time(src.ModTime(ctx)).APIString() fileDate := api.Rfc3339Time(src.ModTime(ctx)).String()
// the allocate request // the allocate request
var request = api.AllocateFileRequest{ var request = api.AllocateFileRequest{