mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2025-10-28 04:05:21 +08:00
711 lines
18 KiB
Go
711 lines
18 KiB
Go
package plg_backend_dav
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"fmt"
|
|
. "github.com/mickael-kerjean/filestash/server/common"
|
|
"io"
|
|
"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 := io.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", name[0])
|
|
}
|
|
}
|
|
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)
|
|
}
|