[to #1] support Nextcloud IOS

This commit is contained in:
2021-10-04 06:06:42 -04:00
parent 8497c9375c
commit a1ebe0a9ed
6 changed files with 533 additions and 73 deletions

View File

@@ -107,18 +107,23 @@ var liveProps = map[xml.Name]struct {
findFn func(context.Context, FileSystem, string, os.FileInfo) (string, error)
// dir is true if the property applies to directories.
dir bool
// file is true if the property applies to files.
file bool
}{
{Space: "DAV:", Local: "resourcetype"}: {
findFn: findResourceType,
dir: true,
file: true,
},
{Space: "DAV:", Local: "displayname"}: {
findFn: findDisplayName,
dir: true,
file: true,
},
{Space: "DAV:", Local: "getcontentlength"}: {
findFn: findContentLength,
dir: false,
file: true,
},
{Space: "DAV:", Local: "getlastmodified"}: {
findFn: findLastModified,
@@ -130,26 +135,143 @@ var liveProps = map[xml.Name]struct {
// sortable by getlastmodified date, so this value is true, not false.
// See golang.org/issue/15334.
dir: true,
file:true,
},
{Space: "DAV:", Local: "creationdate"}: {
findFn: nil,
dir: false,
file: true,
},
{Space: "DAV:", Local: "getcontentlanguage"}: {
findFn: nil,
dir: false,
file: true,
},
{Space: "DAV:", Local: "getcontenttype"}: {
findFn: findContentType,
dir: false,
file: true,
},
{Space: "DAV:", Local: "getetag"}: {
findFn: findETag,
// findETag implements ETag as the concatenated hex values of a file's
// modification time and size. This is not a reliable synchronization
// mechanism for directories, so we do not advertise getetag for DAV
// collections.
dir: true,
file: true,
},
{Space: "DAV:", Local: "quota-available-bytes"}: {
findFn: fnGetQuotaAvailableBytes,
dir: true,
file: false,
},
{Space: "DAV:", Local: "quota-used-bytes"}: {
findFn: fnGetQuotaUsedBytes,
dir: true,
file: false,
},
{Space: "http://owncloud.org/ns", Local: "permissions"}: {
findFn: fnGetPermissions,
dir: true,
file: true,
},
{Space: "http://owncloud.org/ns", Local: "id"}: {
findFn: fnGetID,
dir: true,
file: true,
},
{Space: "http://owncloud.org/ns", Local: "fileid"}: {
findFn: fnGetFileID,
dir: true,
file: true,
},
{Space: "http://owncloud.org/ns", Local: "size"}: {
findFn: fnGetSize,
dir: true,
file: true,
},
{Space: "http://owncloud.org/ns", Local: "favorite"}: {
findFn: fnGetFavorite,
dir: true,
file: true,
},
{Space: "http://owncloud.org/ns", Local: "share-types"}: {
findFn: fnGetShareTypes,
dir: true,
file: true,
},
{Space: "http://owncloud.org/ns", Local: "owner-id"}: {
findFn: fnGetOwnerID,
dir: true,
file: true,
},
{Space: "http://owncloud.org/ns", Local: "owner-display-name"}: {
findFn: fnGetOwnerDisplayName,
dir: true,
file: true,
},
{Space: "http://owncloud.org/ns", Local: "comments-unread"}: {
findFn: fnGetCommentsUnread,
dir: true,
file: true,
},
{Space: "http://owncloud.org/ns", Local: "checksums"}: {
findFn: fnGetChecksums,
dir: false,
file: false,
},
{Space: "http://owncloud.org/ns", Local: "downloadURL"}: {
findFn: fnGetDownloadURL,
dir: false,
file: true,
},
{Space: "http://owncloud.org/ns", Local: "data-fingerprint"}: {
findFn: fnGetDataFingerprint,
dir: true,
file: true,
},
{Space: "http://nextcloud.org/ns", Local: "creation_time"}: {
findFn: fnGetCreationTime,
dir: false,
file: true,
},
{Space: "http://nextcloud.org/ns", Local: "upload_time"}: {
findFn: fnGetUploadTime,
dir: false,
file: true,
},
{Space: "http://nextcloud.org/ns", Local: "is-encrypted"}: {
findFn: fnGetIsEncrypted,
dir: true,
file: false,
},
{Space: "http://nextcloud.org/ns", Local: "has-preview"}: {
findFn: fnGetHasPreview,
dir: true,
file: true,
},
{Space: "http://nextcloud.org/ns", Local: "mount-type"}: {
findFn: fnGetMountType,
dir: true,
file: true,
},
{Space: "http://nextcloud.org/ns", Local: "rich-workspace"}: {
findFn: fnGetRichWorkspace,
dir: false,
file: false,
},
{Space: "http://nextcloud.org/ns", Local: "note"}: {
findFn: fnGetNote,
dir: true,
file: true,
},
{Space: "http://open-collaboration-services.org/ns", Local: "share-permissions"}: {
findFn: fnGetSharePermissionsOCS,
dir: true,
file: true,
},
{Space: "http://open-cloud-mesh.org/ns", Local: "share-permissions"}: {
findFn: fnGetSharePermissionsOCM,
dir: true,
file: true,
},
}
@@ -188,7 +310,8 @@ func props(ctx context.Context, fs FileSystem, name string, pnames []xml.Name) (
continue
}
// Otherwise, it must either be a live property or we don't know it.
if prop := liveProps[pn]; prop.findFn != nil && (prop.dir || !isDir) {
//fmt.Println(liveProps[pn], liveProps[pn].findFn, name, pn)
if prop := liveProps[pn]; prop.findFn != nil && ((prop.dir && isDir) || (prop.file && !isDir)){
innerXML, err := prop.findFn(ctx, fs, name, fi)
if err != nil {
return nil, err
@@ -287,7 +410,7 @@ func escapeXML(s string) string {
func findResourceType(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) {
if fi.IsDir() {
return `<D:collection xmlns:D="DAV:"/>`, nil
return `<d:collection/>`, nil
}
return "", nil
}
@@ -358,33 +481,104 @@ func findContentType(ctx context.Context, fs FileSystem, name string, fi os.File
return ctype, err
}
// ETager is an optional interface for the os.FileInfo objects
// returned by the FileSystem.
//
// If this interface is defined then it will be used to read the ETag
// for the object.
//
// If this interface is not defined an ETag will be computed using the
// ModTime() and the Size() methods of the os.FileInfo object.
type ETager interface {
// ETag returns an ETag for the file. This should be of the
// form "value" or W/"value"
//
// If this returns error ErrNotImplemented then the error will
// be ignored and the base implementation will be used
// instead.
ETag(ctx context.Context) (string, error)
func findETag(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) {
return fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size()), nil
}
func findETag(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) {
if do, ok := fi.(ETager); ok {
etag, err := do.ETag(ctx)
if err != ErrNotImplemented {
return etag, err
}
func fnGetQuotaAvailableBytes(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) {
return "1057129933", nil
}
func fnGetQuotaUsedBytes(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) {
return "16611891", nil
}
func fnGetPermissions(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) {
if fi.IsDir() {
return "RGDNVCK", nil
}
// The Apache http 2.4 web server by default concatenates the
// modification time and size of a file. We replicate the heuristic
// with nanosecond granularity.
return fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size()), nil
return "RGDNVW", nil
}
func fnGetID(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) {
return name, nil
}
func fnGetFileID(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) {
return "0", nil
}
func fnGetSize(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) {
return "16611891", nil
}
func fnGetFavorite(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) {
return "0", nil
}
func fnGetShareTypes(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) {
return "", nil
}
func fnGetOwnerID(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) {
return "vscode", nil
}
func fnGetOwnerDisplayName(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) {
return "vscode", nil
}
func fnGetCommentsUnread(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) {
return "0", nil
}
func fnGetChecksums(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) {
return "", nil
}
func fnGetDownloadURL(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) {
return "", nil
}
func fnGetDataFingerprint(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) {
return "", nil
}
func fnGetCreationTime(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) {
return "0", nil
}
func fnGetUploadTime(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) {
return "0", nil
}
func fnGetIsEncrypted(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) {
return "0", nil
}
func fnGetHasPreview(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) {
return "false", nil
}
func fnGetMountType(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) {
return "", nil
}
func fnGetRichWorkspace(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) {
return "", nil
}
func fnGetNote(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) {
return "", nil
}
func fnGetSharePermissionsOCS(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) {
if fi.IsDir() {
return "31", nil
}
return "19", nil
}
func fnGetSharePermissionsOCM(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) {
return "[\"share\",\"read\",\"write\"]", nil
}

View File

@@ -173,7 +173,19 @@ func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int,
if err != nil {
return http.StatusInternalServerError, err
}
w.Header().Set("ETag", etag)
w.Header().Set("Etag", etag)
w.Header().Set("Oc-Etag", etag)
w.Header().Set("Oc-Fileid", etag)
w.Header().Set("X-Hash-Md5", "97a81b5ce26cfa53e66c21da5c3d60ee")
w.Header().Set("X-Hash-Sha1", "0bdaebd3d8915d4a7de79cdc76cc42ad757a073d")
w.Header().Set("X-Hash-Sha256", "7de442457c55ce163ca64e7952cf4e1c796488f24f2adae8140f4f3160ea94c2")
if r.Header.Get("X-Oc-Ctime") != "" {
w.Header().Set("X-Oc-Ctime", "accepted")
}
if r.Header.Get("X-Oc-Mtime") != "" {
w.Header().Set("X-Oc-Mtime", "accepted")
}
return http.StatusCreated, nil
}
@@ -278,7 +290,6 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status
if err != nil {
return status, err
}
mw := multistatusWriter{w: w}
walkFn := func(reqPath string, info os.FileInfo, err error) error {

View File

@@ -158,31 +158,31 @@ type xmlProperty struct {
}
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_error
// See multistatusWriter for the "D:" namespace prefix.
// See multistatusWriter for the "d:" namespace prefix.
type xmlError struct {
XMLName xml.Name `xml:"D:error"`
XMLName xml.Name `xml:"d:error"`
InnerXML []byte `xml:",innerxml"`
}
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_propstat
// See multistatusWriter for the "D:" namespace prefix.
// See multistatusWriter for the "d:" namespace prefix.
type propstat struct {
Prop []Property `xml:"D:prop>_ignored_"`
Status string `xml:"D:status"`
Error *xmlError `xml:"D:error"`
ResponseDescription string `xml:"D:responsedescription,omitempty"`
Prop []Property `xml:"d:prop>_ignored_"`
Status string `xml:"d:status"`
Error *xmlError `xml:"d:error"`
ResponseDescription string `xml:"d:responsedescription,omitempty"`
}
// xmlPropstat is the same as the propstat type except it holds an xml.Name
// instead of an xml.Name.
type xmlPropstat struct {
Prop []xmlProperty `xml:"D:prop>_ignored_"`
Status string `xml:"D:status"`
Error *xmlError `xml:"D:error"`
ResponseDescription string `xml:"D:responsedescription,omitempty"`
Prop []xmlProperty `xml:"d:prop>_ignored_"`
Status string `xml:"d:status"`
Error *xmlError `xml:"d:error"`
ResponseDescription string `xml:"d:responsedescription,omitempty"`
}
// MarshalXML prepends the "D:" namespace prefix on properties in the DAV: namespace
// MarshalXML prepends the "d:" namespace prefix on properties in the DAV: namespace
// before encoding. See multistatusWriter.
func (ps propstat) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
// Convert from a propstat to an xmlPropstat.
@@ -202,7 +202,19 @@ func (ps propstat) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
for k, prop := range xmlPs.Prop {
if prop.XMLName.Space == "DAV:" {
prop.XMLName = xml.Name{Space: "", Local: "D:" + prop.XMLName.Local}
prop.XMLName = xml.Name{Space: "", Local: "d:" + prop.XMLName.Local}
xmlPs.Prop[k] = prop
} else if prop.XMLName.Space == "http://owncloud.org/ns" {
prop.XMLName = xml.Name{Space: "", Local: "oc:" + prop.XMLName.Local}
xmlPs.Prop[k] = prop
} else if prop.XMLName.Space == "http://nextcloud.org/ns" {
prop.XMLName = xml.Name{Space: "", Local: "nc:" + prop.XMLName.Local}
xmlPs.Prop[k] = prop
} else if prop.XMLName.Space == "http://open-collaboration-services.org/ns" {
prop.XMLName = xml.Name{Space: "http://open-collaboration-services.org/ns", Local: "x1:" + prop.XMLName.Local}
xmlPs.Prop[k] = prop
} else if prop.XMLName.Space == "http://open-cloud-mesh.org/ns" {
prop.XMLName = xml.Name{Space: "http://open-cloud-mesh.org/ns", Local: "x2:" + prop.XMLName.Local}
xmlPs.Prop[k] = prop
}
}
@@ -212,20 +224,20 @@ func (ps propstat) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
}
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_response
// See multistatusWriter for the "D:" namespace prefix.
// See multistatusWriter for the "d:" namespace prefix.
type response struct {
XMLName xml.Name `xml:"D:response"`
Href []string `xml:"D:href"`
Propstat []propstat `xml:"D:propstat"`
Status string `xml:"D:status,omitempty"`
Error *xmlError `xml:"D:error"`
ResponseDescription string `xml:"D:responsedescription,omitempty"`
XMLName xml.Name `xml:"d:response"`
Href []string `xml:"d:href"`
Propstat []propstat `xml:"d:propstat"`
Status string `xml:"d:status,omitempty"`
Error *xmlError `xml:"d:error"`
ResponseDescription string `xml:"d:responsedescription,omitempty"`
}
// MultistatusWriter marshals one or more Responses into a XML
// multistatus response.
// See http://www.webdav.org/specs/rfc4918.html#ELEMENT_multistatus
// TODO(rsto, mpl): As a workaround, the "D:" namespace prefix, defined as
// TODO(rsto, mpl): As a workaround, the "d:" namespace prefix, defined as
// "DAV:" on this element, is prepended on the nested response, as well as on all
// its nested elements. All property names in the DAV: namespace are prefixed as
// well. This is because some versions of Mini-Redirector (on windows 7) ignore
@@ -277,7 +289,8 @@ func (w *multistatusWriter) writeHeader() error {
if w.enc != nil {
return nil
}
w.w.Header().Add("Content-Type", "text/xml; charset=utf-8")
w.w.Header().Add("Content-Type", "application/xml; charset=utf-8")
w.w.Header().Set("DAV", "1, 3, extended-mkcol")
w.w.WriteHeader(http.StatusMultiStatus)
_, err := fmt.Fprintf(w.w, `<?xml version="1.0" encoding="UTF-8"?>`)
if err != nil {
@@ -287,12 +300,22 @@ func (w *multistatusWriter) writeHeader() error {
return w.enc.EncodeToken(xml.StartElement{
Name: xml.Name{
Space: "",
Local: "D:multistatus",
Local: "d:multistatus",
},
Attr: []xml.Attr{{
Name: xml.Name{Space: "", Local: "xmlns:D"},
Name: xml.Name{Space: "", Local: "xmlns:d"},
Value: "DAV:",
}},
},{
Name: xml.Name{Space: "", Local: "xmlns:s"},
Value: "http://sabredav.org/ns",
},{
Name: xml.Name{Space: "", Local: "xmlns:oc"},
Value: "http://owncloud.org/ns",
},{
Name: xml.Name{Space: "", Local: "xmlns:nc"},
Value: "http://nextcloud.org/ns",
},
},
})
}
@@ -314,7 +337,7 @@ func (w *multistatusWriter) close() error {
)
}
end = append(end, xml.EndElement{
Name: xml.Name{Space: "", Local: "D:multistatus"},
Name: xml.Name{Space: "", Local: "d:multistatus"},
})
for _, t := range end {
err := w.enc.EncodeToken(t)