mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2025-11-02 11:57:04 +08:00
In the URL parameters for DAV backends (WebDAV and CalDAV/CardDAV) the
%{username} string is interpolated to the URL encoded username. It
shouldn't conflict with legitimate URLS as %{ is not a valid URL escape
sequence.
This is needed for some servers where the URL contains the username
such as Cyrus IMAP.
712 lines
18 KiB
Go
712 lines
18 KiB
Go
package plg_backend_dav
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"fmt"
|
|
. "github.com/mickael-kerjean/filestash/server/common"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var DavCache AppCache
|
|
|
|
const (
|
|
CARDDAV string = "carddav"
|
|
CALDAV string = "caldav"
|
|
)
|
|
|
|
func init() {
|
|
DavCache = NewAppCache(2, 1)
|
|
Backend.Register(CARDDAV, Dav{})
|
|
Backend.Register(CALDAV, Dav{})
|
|
}
|
|
|
|
type Dav struct {
|
|
which string
|
|
url string
|
|
params map[string]string
|
|
cache map[string]interface{}
|
|
}
|
|
|
|
func (this Dav) Init(params map[string]string, app *App) (IBackend, error) {
|
|
if b := DavCache.Get(params); b != nil {
|
|
backend := b.(*Dav)
|
|
return backend, nil
|
|
}
|
|
backend := Dav{
|
|
url: strings.ReplaceAll(params["url"], "%{username}", url.PathEscape(params["username"])),
|
|
which: params["type"],
|
|
params: params,
|
|
}
|
|
DavCache.Set(params, &backend)
|
|
return backend, nil
|
|
}
|
|
|
|
func (this Dav) LoginForm() Form {
|
|
return Form{
|
|
Elmnts: []FormElement{
|
|
FormElement{
|
|
Name: "type",
|
|
Type: "hidden",
|
|
Value: this.which,
|
|
},
|
|
FormElement{
|
|
Name: "url",
|
|
Type: "text",
|
|
Placeholder: "URL",
|
|
},
|
|
FormElement{
|
|
Name: "username",
|
|
Type: "text",
|
|
Placeholder: "username",
|
|
},
|
|
FormElement{
|
|
Name: "password",
|
|
Type: "password",
|
|
Placeholder: "password",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (this Dav) Ls(path string) ([]os.FileInfo, error) {
|
|
var files []os.FileInfo
|
|
var err error
|
|
|
|
if path == "/" {
|
|
var collections []DavCollection
|
|
if collections, err = this.getCollections(); err != nil {
|
|
return files, err
|
|
}
|
|
files = make([]os.FileInfo, 0, len(collections))
|
|
for _, collection := range collections {
|
|
files = append(files, File{
|
|
FName: collection.Name,
|
|
FType: "directory",
|
|
FSize: -1,
|
|
})
|
|
}
|
|
return files, nil
|
|
}
|
|
|
|
var resources []DavResource
|
|
if resources, err = this.getResources(path); err != nil {
|
|
return files, err
|
|
}
|
|
files = make([]os.FileInfo, 0, len(resources))
|
|
for _, card := range resources {
|
|
files = append(files, File{
|
|
FName: card.Name,
|
|
FType: "file",
|
|
FSize: -1,
|
|
FTime: card.Time,
|
|
})
|
|
}
|
|
return files, nil
|
|
}
|
|
|
|
func (this Dav) Cat(path string) (io.ReadCloser, error) {
|
|
var uri string
|
|
var err error
|
|
var res *http.Response
|
|
|
|
if uri, err = this.getResourceURI(path); err != nil {
|
|
return nil, err
|
|
}
|
|
if res, err = this.request("GET", uri, nil, nil); err != nil {
|
|
return nil, err
|
|
}
|
|
return res.Body, nil
|
|
}
|
|
|
|
func (this Dav) Mkdir(path string) error {
|
|
var uri string
|
|
var err error
|
|
var res *http.Response
|
|
|
|
if uri, err = this.getUserURI(); err != nil {
|
|
return err
|
|
}
|
|
if len(strings.Split(strings.TrimSuffix(strings.TrimPrefix(path, "/"), "/"), "/")) != 1 {
|
|
return ErrNotValid
|
|
} else if _, err = this.getCollectionURI(path); err == nil {
|
|
return ErrConflict
|
|
}
|
|
|
|
name := filepath.Base(path)
|
|
if strings.HasSuffix(uri, "/") {
|
|
uri = uri + name
|
|
} else {
|
|
uri = uri + "/" + name
|
|
}
|
|
if this.which == CARDDAV {
|
|
if res, err = this.request("MKCOL", uri, queryNewAddressBook(name), nil); err != nil {
|
|
return err
|
|
}
|
|
res.Body.Close()
|
|
DavCache.Del(this.params)
|
|
return nil
|
|
} else if this.which == CALDAV {
|
|
if res, err = this.request("MKCOL", uri, queryNewCalendar(name), nil); err != nil {
|
|
return err
|
|
}
|
|
res.Body.Close()
|
|
DavCache.Del(this.params)
|
|
return nil
|
|
}
|
|
return ErrNotValid
|
|
}
|
|
|
|
func (this Dav) Rm(path string) error {
|
|
var uri string
|
|
var err error
|
|
var res *http.Response
|
|
|
|
p := strings.Split(strings.TrimSuffix(strings.TrimPrefix(path, "/"), "/"), "/")
|
|
if len(p) == 1 {
|
|
if uri, err = this.getCollectionURI(path); err != nil {
|
|
return err
|
|
}
|
|
if res, err = this.request("DELETE", uri, nil, nil); err != nil {
|
|
return err
|
|
}
|
|
DavCache.Del(this.params)
|
|
res.Body.Close()
|
|
return nil
|
|
} else if len(p) == 2 {
|
|
if uri, err = this.getResourceURI(path); err != nil {
|
|
return err
|
|
}
|
|
if res, err = this.request("DELETE", uri, nil, nil); err != nil {
|
|
return err
|
|
}
|
|
res.Body.Close()
|
|
return nil
|
|
}
|
|
return ErrNotValid
|
|
}
|
|
|
|
func (this Dav) Mv(from string, to string) error {
|
|
if filepath.Dir(from) != filepath.Dir(to) {
|
|
return ErrNotValid
|
|
}
|
|
reader, err := this.Cat(from)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
d, err := ioutil.ReadAll(reader)
|
|
if err != nil {
|
|
return ErrNotValid
|
|
}
|
|
|
|
content := strings.Split(string(d), "\n")
|
|
for line := range content {
|
|
if this.which == CARDDAV {
|
|
if strings.HasPrefix(content[line], "FN:") {
|
|
content[line] = fmt.Sprintf("FN:%s\n", strings.TrimSuffix(filepath.Base(to), filepath.Ext(to)))
|
|
break
|
|
}
|
|
} else if this.which == CALDAV {
|
|
if strings.HasPrefix(content[line], "SUMMARY:") {
|
|
content[line] = fmt.Sprintf("SUMMARY:%s\n", strings.TrimSuffix(filepath.Base(to), filepath.Ext(to)))
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if err = this.Save(to, strings.NewReader(strings.Join(content, "\n"))); err != nil {
|
|
return err
|
|
}
|
|
return this.Rm(from)
|
|
}
|
|
|
|
func (this Dav) Touch(path string) error {
|
|
var uri string
|
|
var uid string
|
|
var err error
|
|
var res *http.Response
|
|
var content string = ""
|
|
|
|
if uri, err = this.getCollectionURI(path); err != nil {
|
|
return err
|
|
} else if strings.HasSuffix(uri, "/") == false {
|
|
uri += "/"
|
|
}
|
|
uid = RandomString(20)
|
|
uri += uid
|
|
|
|
if this.which == CARDDAV {
|
|
uri += ".vcf"
|
|
name := strings.Split(strings.TrimSuffix(filepath.Base(path), ".vcf"), " ")
|
|
content += "BEGIN:VCARD\n"
|
|
content += "PRODID:-//Filestash//Filestash Carddav client//EN"
|
|
content += "VERSION:3.0\n"
|
|
if len(name) == 1 {
|
|
content += fmt.Sprintf("FN:%s\n", name[0])
|
|
content += fmt.Sprintf("N:;%s;;;\n", name[0])
|
|
} else if len(name) >= 2 {
|
|
content += fmt.Sprintf("FN:%s\n", strings.Join(name, " "))
|
|
if len(name) == 2 {
|
|
content += fmt.Sprintf("N:%s;%s;;;\n", name[0], strings.Join(name[1:], " "))
|
|
} else {
|
|
content += fmt.Sprintf("N:%s\n")
|
|
}
|
|
}
|
|
content += "END:VCARD"
|
|
} else if this.which == CALDAV {
|
|
now := time.Now()
|
|
uri += ".ics"
|
|
name := strings.TrimSuffix(filepath.Base(path), ".ics")
|
|
content += "BEGIN:VCALENDAR\n"
|
|
content += "VERSION:2.0\n"
|
|
content += "PRODID:-//Filestash//Filestash Caldav Client//EN\n"
|
|
content += "BEGIN:VEVENT\n"
|
|
content += fmt.Sprintf("UID:%s\n", uid)
|
|
content += fmt.Sprintf("DTSTART:%04d%02d%02dT080000Z\n", now.Year(), now.Month(), now.Day())
|
|
content += fmt.Sprintf("DTEND:%04d%02d%02dT090000Z\n", now.Year(), now.Month(), now.Day())
|
|
content += fmt.Sprintf("DTSTAMP:%04d%02d%02dT%02d%02d00Z\n", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute())
|
|
content += fmt.Sprintf("SUMMARY:%s\n", name)
|
|
content += "END:VEVENT\n"
|
|
content += "END:VCALENDAR"
|
|
}
|
|
|
|
if content == "" {
|
|
return ErrNotValid
|
|
}
|
|
if res, err = this.request("PUT", uri, strings.NewReader(content), func(req *http.Request) {
|
|
if this.which == CALDAV {
|
|
req.Header.Add("Content-Type", "text/calendar")
|
|
} else if this.which == CARDDAV {
|
|
req.Header.Add("Content-Type", "text/vcard")
|
|
}
|
|
req.Header.Add("If-None-Match", "*")
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
defer res.Body.Close()
|
|
return nil
|
|
}
|
|
|
|
func (this Dav) Save(path string, file io.Reader) error {
|
|
var uriInit string
|
|
var uri string
|
|
var err error
|
|
var res *http.Response
|
|
|
|
if uriInit, err = this.getResourceURI(path); err != nil {
|
|
uriInit = ""
|
|
}
|
|
|
|
if uri, err = this.getCollectionURI(path); err != nil {
|
|
return err
|
|
} else if strings.HasSuffix(uri, "/") == false {
|
|
uri += "/"
|
|
}
|
|
uri += RandomString(15)
|
|
|
|
if this.which == CARDDAV && strings.HasSuffix(uri, ".vcf") == false {
|
|
uri += ".vcf"
|
|
} else if this.which == CALDAV && strings.HasSuffix(uri, ".ics") == false {
|
|
uri += ".ics"
|
|
}
|
|
|
|
if res, err = this.request("PUT", uri, file, func(req *http.Request) {
|
|
if this.which == CALDAV {
|
|
req.Header.Add("Content-Type", "text/calendar")
|
|
} else if this.which == CARDDAV {
|
|
req.Header.Add("Content-Type", "text/vcard")
|
|
}
|
|
req.Header.Add("If-None-Match", "*")
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
res.Body.Close()
|
|
|
|
if uriInit != "" {
|
|
if res, err = this.request("DELETE", uriInit, nil, nil); err != nil {
|
|
return err
|
|
}
|
|
res.Body.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (this Dav) Meta(path string) Metadata {
|
|
m := Metadata{
|
|
CanMove: NewBool(false),
|
|
HideExtension: NewBool(true),
|
|
}
|
|
if path == "/" {
|
|
m.CanCreateFile = NewBool(false)
|
|
m.CanCreateDirectory = NewBool(true)
|
|
m.CanRename = NewBool(false)
|
|
m.CanUpload = NewBool(false)
|
|
m.RefreshOnCreate = NewBool(false)
|
|
} else {
|
|
m.CanCreateFile = NewBool(true)
|
|
m.CanCreateDirectory = NewBool(false)
|
|
m.CanRename = NewBool(true)
|
|
m.CanUpload = NewBool(true)
|
|
m.RefreshOnCreate = NewBool(true)
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (this Dav) request(method string, url string, body io.Reader, fn func(req *http.Request)) (*http.Response, error) {
|
|
req, err := http.NewRequest(method, url, body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if this.params["username"] != "" {
|
|
req.SetBasicAuth(this.params["username"], this.params["password"])
|
|
}
|
|
if req.Body != nil {
|
|
defer req.Body.Close()
|
|
}
|
|
if fn != nil {
|
|
fn(req)
|
|
}
|
|
res, err := HTTPClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if res.StatusCode > 400 {
|
|
res.Body.Close()
|
|
return nil, NewError(HTTPFriendlyStatus(res.StatusCode), res.StatusCode)
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (this Dav) getUserURI() (string, error) {
|
|
var res *http.Response
|
|
var err error
|
|
|
|
if this.cache["getUserURI"] != nil {
|
|
return this.cache["getUserURI"].(string), nil
|
|
}
|
|
|
|
if res, err = this.request(
|
|
"PROPFIND",
|
|
this.url,
|
|
strings.NewReader(`<?xml version="1.0" encoding="utf-8" ?>
|
|
<propfind xmlns="DAV:">
|
|
<prop>
|
|
<current-user-principal />
|
|
<displayname />
|
|
</prop>
|
|
</propfind>`),
|
|
nil,
|
|
); err != nil {
|
|
return "", err
|
|
}
|
|
var t struct {
|
|
Responses []DavCollection `xml:"response"`
|
|
}
|
|
|
|
decoder := xml.NewDecoder(res.Body)
|
|
defer res.Body.Close()
|
|
if err := decoder.Decode(&t); err != nil {
|
|
return "", err
|
|
}
|
|
if len(t.Responses) == 0 {
|
|
return "", ErrNotReachable
|
|
}
|
|
url, err := this.parseURL(t.Responses[0].User)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if this.cache == nil {
|
|
this.cache = make(map[string]interface{})
|
|
}
|
|
this.cache["getUserURI"] = url
|
|
DavCache.Set(this.params, &this)
|
|
return url, nil
|
|
}
|
|
|
|
func (this Dav) getCollections() ([]DavCollection, error) {
|
|
var uri string
|
|
var res *http.Response
|
|
var err error
|
|
|
|
if this.cache["getCollections"] != nil {
|
|
return this.cache["getCollections"].([]DavCollection), nil
|
|
}
|
|
|
|
if uri, err = this.getUserURI(); err != nil {
|
|
return nil, err
|
|
}
|
|
if res, err = this.request("PROPFIND", uri, strings.NewReader(`<?xml version='1.0' encoding='UTF-8' ?>
|
|
<propfind xmlns="DAV:">
|
|
<prop>
|
|
<resourcetype />
|
|
<getcontenttype/>
|
|
<displayname />
|
|
</prop>
|
|
</propfind>`), func(req *http.Request) {
|
|
req.Header.Add("Depth", "1")
|
|
req.Header.Add("Content-Type", "application/xml")
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var t struct {
|
|
Responses []DavCollection `xml:"response"`
|
|
}
|
|
decoder := xml.NewDecoder(res.Body)
|
|
defer res.Body.Close()
|
|
if err := decoder.Decode(&t); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var collections []DavCollection = make([]DavCollection, 0, len(t.Responses))
|
|
for i := range t.Responses {
|
|
if t.Responses[i].Name == "" {
|
|
continue
|
|
} else if this.which == CARDDAV {
|
|
if strings.Contains(t.Responses[i].Type.Inner, "addressbook") {
|
|
collections = append(collections, t.Responses[i])
|
|
}
|
|
} else if this.which == CALDAV {
|
|
if strings.Contains(t.Responses[i].Type.Inner, "calendar") {
|
|
collections = append(collections, t.Responses[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
if this.cache == nil {
|
|
this.cache = make(map[string]interface{})
|
|
}
|
|
this.cache["getCollections"] = collections
|
|
DavCache.Set(this.params, &this)
|
|
return collections, nil
|
|
}
|
|
|
|
func (this Dav) getCollectionURI(path string) (string, error) {
|
|
path = strings.TrimSuffix(strings.TrimPrefix(path, "/"), "/")
|
|
p := strings.Split(path, "/")
|
|
if len(p) == 0 {
|
|
return "", ErrNotFound
|
|
}
|
|
coll, err := this.getCollections()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for i := 0; i < len(coll); i++ {
|
|
if coll[i].Name == string(p[0]) {
|
|
return this.parseURL(coll[i].Url)
|
|
}
|
|
}
|
|
return "", ErrNotFound
|
|
}
|
|
|
|
func (this Dav) getResources(path string) ([]DavResource, error) {
|
|
var uri string
|
|
var res *http.Response
|
|
var err error
|
|
|
|
if uri, err = this.getCollectionURI(path); err != nil {
|
|
return nil, err
|
|
}
|
|
if res, err = this.request(
|
|
"REPORT",
|
|
uri,
|
|
func() io.Reader {
|
|
var query string = ""
|
|
if this.which == CARDDAV {
|
|
query = `<C:addressbook-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
|
|
<D:prop>
|
|
<C:address-data>
|
|
<C:prop name="FN"/>
|
|
<C:prop name="REV"/>
|
|
</C:address-data>
|
|
</D:prop>
|
|
</C:addressbook-query>`
|
|
} else if this.which == CALDAV {
|
|
query = `<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
|
|
<D:prop>
|
|
<C:calendar-data>
|
|
<C:prop name="SUMMARY"/>
|
|
</C:calendar-data>
|
|
</D:prop>
|
|
</C:calendar-query>`
|
|
}
|
|
return strings.NewReader(query)
|
|
}(),
|
|
func(req *http.Request) {
|
|
req.Header.Add("Depth", "1")
|
|
req.Header.Add("Content-Type", "application/xml")
|
|
},
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
decoder := xml.NewDecoder(res.Body)
|
|
defer res.Body.Close()
|
|
var r struct {
|
|
Responses []DavResource `xml:"response"`
|
|
}
|
|
if err = decoder.Decode(&r); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for i := range r.Responses {
|
|
r.Responses[i].Name, r.Responses[i].Time = func() (string, int64) {
|
|
var t int64 = 0
|
|
name := "unknown"
|
|
if this.which == CARDDAV {
|
|
for _, line := range strings.Split(r.Responses[i].Vcard, "\n") {
|
|
if strings.HasPrefix(line, "FN:") && this.which == CARDDAV {
|
|
name = strings.TrimPrefix(line, "FN:")
|
|
break
|
|
}
|
|
}
|
|
name += ".vcf"
|
|
} else if this.which == CALDAV {
|
|
strToInt := func(chunk string) int {
|
|
ret, _ := strconv.Atoi(chunk)
|
|
return ret
|
|
}
|
|
for _, line := range strings.Split(r.Responses[i].Ical, "\n") {
|
|
if strings.HasPrefix(line, "SUMMARY:") {
|
|
name = strings.TrimPrefix(line, "SUMMARY:")
|
|
} else if strings.HasPrefix(line, "DTSTART:") {
|
|
// https://tools.ietf.org/html/rfc2445#section-4.3.5
|
|
// quick and dirty parser for form 1 & 2
|
|
c := strings.TrimSuffix(strings.TrimSpace(strings.TrimPrefix(line, "DTSTART:")), "Z")
|
|
if len(c) == 15 && t == 0 {
|
|
t = time.Date(
|
|
strToInt(c[0:4]), time.Month(strToInt(c[4:6])+1), strToInt(c[6:8]), // date
|
|
strToInt(c[9:11]), strToInt(c[11:13]), strToInt(c[13:15]), // time
|
|
0, time.UTC,
|
|
).Unix()
|
|
}
|
|
} else if strings.HasPrefix(line, "DTSTART;VALUE=DATE:") {
|
|
c := strings.TrimSpace(strings.TrimPrefix(line, "DTSTART;VALUE=DATE:"))
|
|
if len(c) == 8 && t == 0 {
|
|
t = time.Date(
|
|
strToInt(c[0:4]), time.Month(strToInt(c[4:6])+1), strToInt(c[6:8]), // date
|
|
0, 0, 0, // time
|
|
0, time.UTC,
|
|
).Unix()
|
|
}
|
|
}
|
|
}
|
|
name += ".ics"
|
|
}
|
|
return name, t
|
|
}()
|
|
}
|
|
return r.Responses, nil
|
|
}
|
|
|
|
func (this Dav) getResourceURI(path string) (string, error) {
|
|
path = strings.TrimSuffix(strings.TrimPrefix(path, "/"), "/")
|
|
p := strings.Split(path, "/")
|
|
if len(p) != 2 {
|
|
return "", ErrNotValid
|
|
}
|
|
|
|
var resources []DavResource
|
|
var err error
|
|
if resources, err = this.getResources(path); err != nil {
|
|
return "", ErrNotFound
|
|
}
|
|
filename := filepath.Base(path)
|
|
for i := range resources {
|
|
if resources[i].Name == filename {
|
|
return this.parseURL(resources[i].Url)
|
|
}
|
|
}
|
|
return "", ErrNotFound
|
|
}
|
|
|
|
func (this Dav) parseURL(link string) (string, error) {
|
|
var origin *url.URL
|
|
var destination *url.URL
|
|
var err error
|
|
|
|
if destination, _ = url.Parse(link); err != nil {
|
|
return "", err
|
|
}
|
|
if origin, err = url.Parse(this.url); err != nil {
|
|
return "", err
|
|
}
|
|
if destination.Host == "" || destination.Scheme == "" {
|
|
destination.Host = origin.Host
|
|
destination.Scheme = origin.Scheme
|
|
}
|
|
return destination.String(), nil
|
|
}
|
|
|
|
func joinURL(base string, bit string) string {
|
|
if strings.HasSuffix(base, "/") == false {
|
|
base += "/"
|
|
}
|
|
return base + bit
|
|
}
|
|
|
|
type DavResource struct {
|
|
Url string `xml:"href"`
|
|
Name string `xml:"-"`
|
|
Time int64 `xml:"-"`
|
|
Vcard string `xml:"propstat>prop>address-data,omitempty"`
|
|
Ical string `xml:"propstat>prop>calendar-data,omitempty"`
|
|
}
|
|
|
|
type DavCollection struct {
|
|
Url string `xml:"href"`
|
|
Name string `xml:"propstat>prop>displayname,omitempty"`
|
|
User string `xml:"propstat>prop>current-user-principal>href,omitempty"`
|
|
Type struct {
|
|
Inner string `xml:",innerxml"`
|
|
} `xml:"propstat>prop>resourcetype,omitempty"`
|
|
}
|
|
|
|
func queryNewCalendar(name string) io.Reader {
|
|
query := `<?xml version="1.0" encoding="UTF-8" ?>
|
|
<create xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav" xmlns:I="http://apple.com/ns/ical/">
|
|
<set>
|
|
<prop>
|
|
<resourcetype>
|
|
<collection />
|
|
<C:calendar />
|
|
</resourcetype>
|
|
<C:supported-calendar-component-set>
|
|
<C:comp name="VEVENT" />
|
|
<C:comp name="VJOURNAL" />
|
|
<C:comp name="VTODO" />
|
|
</C:supported-calendar-component-set>
|
|
<displayname>{{NAME}}</displayname>
|
|
<C:calendar-description></C:calendar-description>
|
|
<I:calendar-color>#9AD1ED</I:calendar-color>
|
|
</prop>
|
|
</set>
|
|
</create>`
|
|
query = strings.Replace(query, "{{NAME}}", name, -1)
|
|
return strings.NewReader(query)
|
|
}
|
|
|
|
func queryNewAddressBook(name string) io.Reader {
|
|
query := `<?xml version="1.0" encoding="UTF-8" ?>
|
|
<create xmlns="DAV:" xmlns:CR="urn:ietf:params:xml:ns:carddav">
|
|
<set>
|
|
<prop>
|
|
<resourcetype>
|
|
<collection />
|
|
<CR:addressbook />
|
|
</resourcetype>
|
|
<displayname>{{NAME}}</displayname>
|
|
<CR:addressbook-description></CR:addressbook-description>
|
|
</prop>
|
|
</set>
|
|
</create>`
|
|
query = strings.Replace(query, "{{NAME}}", name, -1)
|
|
return strings.NewReader(query)
|
|
}
|