mirror of
https://github.com/rclone/rclone.git
synced 2024-11-25 01:44:41 +01:00
914 lines
27 KiB
Go
914 lines
27 KiB
Go
|
package api
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"context"
|
||
|
"io"
|
||
|
"mime"
|
||
|
"net/http"
|
||
|
"net/url"
|
||
|
"path/filepath"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
"github.com/google/uuid"
|
||
|
"github.com/rclone/rclone/fs"
|
||
|
"github.com/rclone/rclone/lib/rest"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
defaultZone = "com.apple.CloudDocs"
|
||
|
statusOk = "OK"
|
||
|
statusEtagConflict = "ETAG_CONFLICT"
|
||
|
)
|
||
|
|
||
|
// DriveService represents an iCloud Drive service.
|
||
|
type DriveService struct {
|
||
|
icloud *Client
|
||
|
RootID string
|
||
|
endpoint string
|
||
|
docsEndpoint string
|
||
|
}
|
||
|
|
||
|
// NewDriveService creates a new DriveService instance.
|
||
|
func NewDriveService(icloud *Client) (*DriveService, error) {
|
||
|
return &DriveService{icloud: icloud, RootID: "FOLDER::com.apple.CloudDocs::root", endpoint: icloud.Session.AccountInfo.Webservices["drivews"].URL, docsEndpoint: icloud.Session.AccountInfo.Webservices["docws"].URL}, nil
|
||
|
}
|
||
|
|
||
|
// GetItemByDriveID retrieves a DriveItem by its Drive ID.
|
||
|
func (d *DriveService) GetItemByDriveID(ctx context.Context, id string, includeChildren bool) (*DriveItem, *http.Response, error) {
|
||
|
items, resp, err := d.GetItemsByDriveID(ctx, []string{id}, includeChildren)
|
||
|
if err != nil {
|
||
|
return nil, resp, err
|
||
|
}
|
||
|
return items[0], resp, err
|
||
|
}
|
||
|
|
||
|
// GetItemsByDriveID retrieves DriveItems by their Drive IDs.
|
||
|
func (d *DriveService) GetItemsByDriveID(ctx context.Context, ids []string, includeChildren bool) ([]*DriveItem, *http.Response, error) {
|
||
|
var err error
|
||
|
_items := []map[string]any{}
|
||
|
for _, id := range ids {
|
||
|
_items = append(_items, map[string]any{
|
||
|
"drivewsid": id,
|
||
|
"partialData": false,
|
||
|
"includeHierarchy": false,
|
||
|
})
|
||
|
}
|
||
|
|
||
|
var body *bytes.Reader
|
||
|
var path string
|
||
|
if !includeChildren {
|
||
|
values := []map[string]any{{
|
||
|
"items": _items,
|
||
|
}}
|
||
|
body, err = IntoReader(values)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
path = "/retrieveItemDetails"
|
||
|
} else {
|
||
|
values := _items
|
||
|
body, err = IntoReader(values)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
path = "/retrieveItemDetailsInFolders"
|
||
|
}
|
||
|
|
||
|
opts := rest.Opts{
|
||
|
Method: "POST",
|
||
|
Path: path,
|
||
|
ExtraHeaders: d.icloud.Session.GetHeaders(map[string]string{}),
|
||
|
RootURL: d.endpoint,
|
||
|
Body: body,
|
||
|
}
|
||
|
var items []*DriveItem
|
||
|
resp, err := d.icloud.Request(ctx, opts, nil, &items)
|
||
|
if err != nil {
|
||
|
return nil, resp, err
|
||
|
}
|
||
|
|
||
|
return items, resp, err
|
||
|
}
|
||
|
|
||
|
// GetDocByPath retrieves a document by its path.
|
||
|
func (d *DriveService) GetDocByPath(ctx context.Context, path string) (*Document, *http.Response, error) {
|
||
|
values := url.Values{}
|
||
|
values.Set("unified_format", "false")
|
||
|
body, err := IntoReader(path)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
opts := rest.Opts{
|
||
|
Method: "POST",
|
||
|
Path: "/ws/" + defaultZone + "/list/lookup_by_path",
|
||
|
ExtraHeaders: d.icloud.Session.GetHeaders(map[string]string{}),
|
||
|
RootURL: d.docsEndpoint,
|
||
|
Parameters: values,
|
||
|
Body: body,
|
||
|
}
|
||
|
var item []*Document
|
||
|
resp, err := d.icloud.Request(ctx, opts, nil, &item)
|
||
|
if err != nil {
|
||
|
return nil, resp, err
|
||
|
}
|
||
|
|
||
|
return item[0], resp, err
|
||
|
}
|
||
|
|
||
|
// GetItemByPath retrieves a DriveItem by its path.
|
||
|
func (d *DriveService) GetItemByPath(ctx context.Context, path string) (*DriveItem, *http.Response, error) {
|
||
|
values := url.Values{}
|
||
|
values.Set("unified_format", "true")
|
||
|
|
||
|
body, err := IntoReader(path)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
opts := rest.Opts{
|
||
|
Method: "POST",
|
||
|
Path: "/ws/" + defaultZone + "/list/lookup_by_path",
|
||
|
ExtraHeaders: d.icloud.Session.GetHeaders(map[string]string{}),
|
||
|
RootURL: d.docsEndpoint,
|
||
|
Parameters: values,
|
||
|
Body: body,
|
||
|
}
|
||
|
var item []*DriveItem
|
||
|
resp, err := d.icloud.Request(ctx, opts, nil, &item)
|
||
|
if err != nil {
|
||
|
return nil, resp, err
|
||
|
}
|
||
|
|
||
|
return item[0], resp, err
|
||
|
}
|
||
|
|
||
|
// GetDocByItemID retrieves a document by its item ID.
|
||
|
func (d *DriveService) GetDocByItemID(ctx context.Context, id string) (*Document, *http.Response, error) {
|
||
|
values := url.Values{}
|
||
|
values.Set("document_id", id)
|
||
|
values.Set("unified_format", "false") // important
|
||
|
opts := rest.Opts{
|
||
|
Method: "GET",
|
||
|
Path: "/ws/" + defaultZone + "/list/lookup_by_id",
|
||
|
ExtraHeaders: d.icloud.Session.GetHeaders(map[string]string{}),
|
||
|
RootURL: d.docsEndpoint,
|
||
|
Parameters: values,
|
||
|
}
|
||
|
var item *Document
|
||
|
resp, err := d.icloud.Request(ctx, opts, nil, &item)
|
||
|
if err != nil {
|
||
|
return nil, resp, err
|
||
|
}
|
||
|
|
||
|
return item, resp, err
|
||
|
}
|
||
|
|
||
|
// GetItemRawByItemID retrieves a DriveItemRaw by its item ID.
|
||
|
func (d *DriveService) GetItemRawByItemID(ctx context.Context, id string) (*DriveItemRaw, *http.Response, error) {
|
||
|
opts := rest.Opts{
|
||
|
Method: "GET",
|
||
|
Path: "/v1/item/" + id,
|
||
|
ExtraHeaders: d.icloud.Session.GetHeaders(map[string]string{}),
|
||
|
RootURL: d.docsEndpoint,
|
||
|
}
|
||
|
var item *DriveItemRaw
|
||
|
resp, err := d.icloud.Request(ctx, opts, nil, &item)
|
||
|
if err != nil {
|
||
|
return nil, resp, err
|
||
|
}
|
||
|
|
||
|
return item, resp, err
|
||
|
}
|
||
|
|
||
|
// GetItemsInFolder retrieves a list of DriveItemRaw objects in a folder with the given ID.
|
||
|
func (d *DriveService) GetItemsInFolder(ctx context.Context, id string, limit int64) ([]*DriveItemRaw, *http.Response, error) {
|
||
|
values := url.Values{}
|
||
|
values.Set("limit", strconv.FormatInt(limit, 10))
|
||
|
|
||
|
opts := rest.Opts{
|
||
|
Method: "GET",
|
||
|
Path: "/v1/enumerate/" + id,
|
||
|
ExtraHeaders: d.icloud.Session.GetHeaders(map[string]string{}),
|
||
|
RootURL: d.docsEndpoint,
|
||
|
Parameters: values,
|
||
|
}
|
||
|
|
||
|
items := struct {
|
||
|
Items []*DriveItemRaw `json:"drive_item"`
|
||
|
}{}
|
||
|
|
||
|
resp, err := d.icloud.Request(ctx, opts, nil, &items)
|
||
|
if err != nil {
|
||
|
return nil, resp, err
|
||
|
}
|
||
|
|
||
|
return items.Items, resp, err
|
||
|
}
|
||
|
|
||
|
// GetDownloadURLByDriveID retrieves the download URL for a file in the DriveService.
|
||
|
func (d *DriveService) GetDownloadURLByDriveID(ctx context.Context, id string) (string, *http.Response, error) {
|
||
|
_, zone, docid := DeconstructDriveID(id)
|
||
|
values := url.Values{}
|
||
|
values.Set("document_id", docid)
|
||
|
|
||
|
if zone == "" {
|
||
|
zone = defaultZone
|
||
|
}
|
||
|
|
||
|
opts := rest.Opts{
|
||
|
Method: "GET",
|
||
|
Path: "/ws/" + zone + "/download/by_id",
|
||
|
Parameters: values,
|
||
|
ExtraHeaders: d.icloud.Session.GetHeaders(map[string]string{}),
|
||
|
RootURL: d.docsEndpoint,
|
||
|
}
|
||
|
|
||
|
var filer *FileRequest
|
||
|
resp, err := d.icloud.Request(ctx, opts, nil, &filer)
|
||
|
|
||
|
if err != nil {
|
||
|
return "", resp, err
|
||
|
}
|
||
|
|
||
|
var url string
|
||
|
if filer.DataToken != nil {
|
||
|
url = filer.DataToken.URL
|
||
|
} else {
|
||
|
url = filer.PackageToken.URL
|
||
|
}
|
||
|
|
||
|
return url, resp, err
|
||
|
}
|
||
|
|
||
|
// DownloadFile downloads a file from the given URL using the provided options.
|
||
|
func (d *DriveService) DownloadFile(ctx context.Context, url string, opt []fs.OpenOption) (*http.Response, error) {
|
||
|
opts := &rest.Opts{
|
||
|
Method: "GET",
|
||
|
ExtraHeaders: d.icloud.Session.GetHeaders(map[string]string{}),
|
||
|
RootURL: url,
|
||
|
Options: opt,
|
||
|
}
|
||
|
|
||
|
resp, err := d.icloud.srv.Call(ctx, opts)
|
||
|
if err != nil {
|
||
|
// icloud has some weird http codes
|
||
|
if resp.StatusCode == 330 {
|
||
|
loc, err := resp.Location()
|
||
|
if err == nil {
|
||
|
return d.DownloadFile(ctx, loc.String(), opt)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return resp, err
|
||
|
}
|
||
|
return d.icloud.srv.Call(ctx, opts)
|
||
|
}
|
||
|
|
||
|
// MoveItemToTrashByItemID moves an item to the trash based on the item ID.
|
||
|
func (d *DriveService) MoveItemToTrashByItemID(ctx context.Context, id, etag string, force bool) (*DriveItem, *http.Response, error) {
|
||
|
doc, resp, err := d.GetDocByItemID(ctx, id)
|
||
|
if err != nil {
|
||
|
return nil, resp, err
|
||
|
}
|
||
|
return d.MoveItemToTrashByID(ctx, doc.DriveID(), etag, force)
|
||
|
}
|
||
|
|
||
|
// MoveItemToTrashByID moves an item to the trash based on the item ID.
|
||
|
func (d *DriveService) MoveItemToTrashByID(ctx context.Context, drivewsid, etag string, force bool) (*DriveItem, *http.Response, error) {
|
||
|
values := map[string]any{
|
||
|
"items": []map[string]any{{
|
||
|
"drivewsid": drivewsid,
|
||
|
"etag": etag,
|
||
|
"clientId": drivewsid,
|
||
|
}}}
|
||
|
|
||
|
body, err := IntoReader(values)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
|
||
|
opts := rest.Opts{
|
||
|
Method: "POST",
|
||
|
Path: "/moveItemsToTrash",
|
||
|
ExtraHeaders: d.icloud.Session.GetHeaders(map[string]string{}),
|
||
|
RootURL: d.endpoint,
|
||
|
Body: body,
|
||
|
}
|
||
|
|
||
|
item := struct {
|
||
|
Items []*DriveItem `json:"items"`
|
||
|
}{}
|
||
|
resp, err := d.icloud.Request(ctx, opts, nil, &item)
|
||
|
|
||
|
if err != nil {
|
||
|
return nil, resp, err
|
||
|
}
|
||
|
|
||
|
if item.Items[0].Status != statusOk {
|
||
|
// rerun with latest etag
|
||
|
if force && item.Items[0].Status == "ETAG_CONFLICT" {
|
||
|
return d.MoveItemToTrashByID(ctx, drivewsid, item.Items[0].Etag, false)
|
||
|
}
|
||
|
|
||
|
err = newRequestError(item.Items[0].Status, "unknown request status")
|
||
|
}
|
||
|
|
||
|
return item.Items[0], resp, err
|
||
|
}
|
||
|
|
||
|
// CreateNewFolderByItemID creates a new folder by item ID.
|
||
|
func (d *DriveService) CreateNewFolderByItemID(ctx context.Context, id, name string) (*DriveItem, *http.Response, error) {
|
||
|
doc, resp, err := d.GetDocByItemID(ctx, id)
|
||
|
if err != nil {
|
||
|
return nil, resp, err
|
||
|
}
|
||
|
return d.CreateNewFolderByDriveID(ctx, doc.DriveID(), name)
|
||
|
}
|
||
|
|
||
|
// CreateNewFolderByDriveID creates a new folder by its Drive ID.
|
||
|
func (d *DriveService) CreateNewFolderByDriveID(ctx context.Context, drivewsid, name string) (*DriveItem, *http.Response, error) {
|
||
|
values := map[string]any{
|
||
|
"destinationDrivewsId": drivewsid,
|
||
|
"folders": []map[string]any{{
|
||
|
"clientId": "FOLDER::UNKNOWN_ZONE::TempId-" + uuid.New().String(),
|
||
|
"name": name,
|
||
|
}},
|
||
|
}
|
||
|
|
||
|
body, err := IntoReader(values)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
|
||
|
opts := rest.Opts{
|
||
|
Method: "POST",
|
||
|
Path: "/createFolders",
|
||
|
ExtraHeaders: d.icloud.Session.GetHeaders(map[string]string{}),
|
||
|
RootURL: d.endpoint,
|
||
|
Body: body,
|
||
|
}
|
||
|
var fResp *CreateFoldersResponse
|
||
|
resp, err := d.icloud.Request(ctx, opts, nil, &fResp)
|
||
|
if err != nil {
|
||
|
return nil, resp, err
|
||
|
}
|
||
|
status := fResp.Folders[0].Status
|
||
|
if status != statusOk {
|
||
|
err = newRequestError(status, "unknown request status")
|
||
|
}
|
||
|
|
||
|
return fResp.Folders[0], resp, err
|
||
|
}
|
||
|
|
||
|
// RenameItemByItemID renames a DriveItem by its item ID.
|
||
|
func (d *DriveService) RenameItemByItemID(ctx context.Context, id, etag, name string, force bool) (*DriveItem, *http.Response, error) {
|
||
|
doc, resp, err := d.GetDocByItemID(ctx, id)
|
||
|
if err != nil {
|
||
|
return nil, resp, err
|
||
|
}
|
||
|
return d.RenameItemByDriveID(ctx, doc.DriveID(), doc.Etag, name, force)
|
||
|
}
|
||
|
|
||
|
// RenameItemByDriveID renames a DriveItem by its drive ID.
|
||
|
func (d *DriveService) RenameItemByDriveID(ctx context.Context, id, etag, name string, force bool) (*DriveItem, *http.Response, error) {
|
||
|
values := map[string]any{
|
||
|
"items": []map[string]any{{
|
||
|
"drivewsid": id,
|
||
|
"name": name,
|
||
|
"etag": etag,
|
||
|
// "extension": split[1],
|
||
|
}},
|
||
|
}
|
||
|
|
||
|
body, err := IntoReader(values)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
|
||
|
opts := rest.Opts{
|
||
|
Method: "POST",
|
||
|
Path: "/renameItems",
|
||
|
ExtraHeaders: d.icloud.Session.GetHeaders(map[string]string{}),
|
||
|
RootURL: d.endpoint,
|
||
|
Body: body,
|
||
|
}
|
||
|
var items *DriveItem
|
||
|
resp, err := d.icloud.Request(ctx, opts, nil, &items)
|
||
|
|
||
|
if err != nil {
|
||
|
return nil, resp, err
|
||
|
}
|
||
|
|
||
|
status := items.Items[0].Status
|
||
|
if status != statusOk {
|
||
|
// rerun with latest etag
|
||
|
if force && status == "ETAG_CONFLICT" {
|
||
|
return d.RenameItemByDriveID(ctx, id, items.Items[0].Etag, name, false)
|
||
|
}
|
||
|
err = newRequestErrorf(status, "unknown inner status for: %s %s", opts.Method, resp.Request.URL)
|
||
|
}
|
||
|
|
||
|
return items.Items[0], resp, err
|
||
|
}
|
||
|
|
||
|
// MoveItemByItemID moves an item by its item ID to a destination item ID.
|
||
|
func (d *DriveService) MoveItemByItemID(ctx context.Context, id, etag, dstID string, force bool) (*DriveItem, *http.Response, error) {
|
||
|
docSrc, resp, err := d.GetDocByItemID(ctx, id)
|
||
|
if err != nil {
|
||
|
return nil, resp, err
|
||
|
}
|
||
|
docDst, resp, err := d.GetDocByItemID(ctx, dstID)
|
||
|
if err != nil {
|
||
|
return nil, resp, err
|
||
|
}
|
||
|
return d.MoveItemByDriveID(ctx, docSrc.DriveID(), docSrc.Etag, docDst.DriveID(), force)
|
||
|
}
|
||
|
|
||
|
// MoveItemByDocID moves an item by its doc ID.
|
||
|
// func (d *DriveService) MoveItemByDocID(ctx context.Context, srcDocID, srcEtag, dstDocID string, force bool) (*DriveItem, *http.Response, error) {
|
||
|
// return d.MoveItemByDriveID(ctx, srcDocID, srcEtag, docDst.DriveID(), force)
|
||
|
// }
|
||
|
|
||
|
// MoveItemByDriveID moves an item by its drive ID.
|
||
|
func (d *DriveService) MoveItemByDriveID(ctx context.Context, id, etag, dstID string, force bool) (*DriveItem, *http.Response, error) {
|
||
|
values := map[string]any{
|
||
|
"destinationDrivewsId": dstID,
|
||
|
"items": []map[string]any{{
|
||
|
"drivewsid": id,
|
||
|
"etag": etag,
|
||
|
"clientId": id,
|
||
|
}},
|
||
|
}
|
||
|
|
||
|
body, err := IntoReader(values)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
|
||
|
opts := rest.Opts{
|
||
|
Method: "POST",
|
||
|
Path: "/moveItems",
|
||
|
ExtraHeaders: d.icloud.Session.GetHeaders(map[string]string{}),
|
||
|
RootURL: d.endpoint,
|
||
|
Body: body,
|
||
|
}
|
||
|
|
||
|
var items *DriveItem
|
||
|
resp, err := d.icloud.Request(ctx, opts, nil, &items)
|
||
|
|
||
|
if err != nil {
|
||
|
return nil, resp, err
|
||
|
}
|
||
|
|
||
|
status := items.Items[0].Status
|
||
|
if status != statusOk {
|
||
|
// rerun with latest etag
|
||
|
if force && status == "ETAG_CONFLICT" {
|
||
|
return d.MoveItemByDriveID(ctx, id, items.Items[0].Etag, dstID, false)
|
||
|
}
|
||
|
err = newRequestErrorf(status, "unknown inner status for: %s %s", opts.Method, resp.Request.URL)
|
||
|
}
|
||
|
|
||
|
return items.Items[0], resp, err
|
||
|
}
|
||
|
|
||
|
// CopyDocByItemID copies a document by its item ID.
|
||
|
func (d *DriveService) CopyDocByItemID(ctx context.Context, itemID string) (*DriveItemRaw, *http.Response, error) {
|
||
|
// putting name in info doesnt work. extension does work so assume this is a bug in the endpoint
|
||
|
values := map[string]any{
|
||
|
"info_to_update": map[string]any{},
|
||
|
}
|
||
|
|
||
|
body, err := IntoReader(values)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
opts := rest.Opts{
|
||
|
Method: "POST",
|
||
|
Path: "/v1/item/copy/" + itemID,
|
||
|
ExtraHeaders: d.icloud.Session.GetHeaders(map[string]string{}),
|
||
|
RootURL: d.docsEndpoint,
|
||
|
Body: body,
|
||
|
}
|
||
|
|
||
|
var info *DriveItemRaw
|
||
|
resp, err := d.icloud.Request(ctx, opts, nil, &info)
|
||
|
if err != nil {
|
||
|
return nil, resp, err
|
||
|
}
|
||
|
return info, resp, err
|
||
|
}
|
||
|
|
||
|
// CreateUpload creates an url for an upload.
|
||
|
func (d *DriveService) CreateUpload(ctx context.Context, size int64, name string) (*UploadResponse, *http.Response, error) {
|
||
|
// first we need to request an upload url
|
||
|
values := map[string]any{
|
||
|
"filename": name,
|
||
|
"type": "FILE",
|
||
|
"size": strconv.FormatInt(size, 10),
|
||
|
"content_type": GetContentTypeForFile(name),
|
||
|
}
|
||
|
body, err := IntoReader(values)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
|
||
|
opts := rest.Opts{
|
||
|
Method: "POST",
|
||
|
Path: "/ws/" + defaultZone + "/upload/web",
|
||
|
ExtraHeaders: d.icloud.Session.GetHeaders(map[string]string{}),
|
||
|
RootURL: d.docsEndpoint,
|
||
|
Body: body,
|
||
|
}
|
||
|
var responseInfo []*UploadResponse
|
||
|
resp, err := d.icloud.Request(ctx, opts, nil, &responseInfo)
|
||
|
if err != nil {
|
||
|
return nil, resp, err
|
||
|
}
|
||
|
return responseInfo[0], resp, err
|
||
|
}
|
||
|
|
||
|
// Upload uploads a file to the given url
|
||
|
func (d *DriveService) Upload(ctx context.Context, in io.Reader, size int64, name, uploadURL string) (*SingleFileResponse, *http.Response, error) {
|
||
|
// TODO: implement multipart upload
|
||
|
opts := rest.Opts{
|
||
|
Method: "POST",
|
||
|
ExtraHeaders: d.icloud.Session.GetHeaders(map[string]string{}),
|
||
|
RootURL: uploadURL,
|
||
|
Body: in,
|
||
|
ContentLength: &size,
|
||
|
ContentType: GetContentTypeForFile(name),
|
||
|
// MultipartContentName: "files",
|
||
|
MultipartFileName: name,
|
||
|
}
|
||
|
var singleFileResponse *SingleFileResponse
|
||
|
resp, err := d.icloud.Request(ctx, opts, nil, &singleFileResponse)
|
||
|
if err != nil {
|
||
|
return nil, resp, err
|
||
|
}
|
||
|
return singleFileResponse, resp, err
|
||
|
}
|
||
|
|
||
|
// UpdateFile updates a file in the DriveService.
|
||
|
//
|
||
|
// ctx: the context.Context object for the request.
|
||
|
// r: a pointer to the UpdateFileInfo struct containing the information for the file update.
|
||
|
// Returns a pointer to the DriveItem struct representing the updated file, the http.Response object, and an error if any.
|
||
|
func (d *DriveService) UpdateFile(ctx context.Context, r *UpdateFileInfo) (*DriveItem, *http.Response, error) {
|
||
|
body, err := IntoReader(r)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
opts := rest.Opts{
|
||
|
Method: "POST",
|
||
|
Path: "/ws/" + defaultZone + "/update/documents",
|
||
|
ExtraHeaders: d.icloud.Session.GetHeaders(map[string]string{}),
|
||
|
RootURL: d.docsEndpoint,
|
||
|
Body: body,
|
||
|
}
|
||
|
var responseInfo *DocumentUpdateResponse
|
||
|
resp, err := d.icloud.Request(ctx, opts, nil, &responseInfo)
|
||
|
if err != nil {
|
||
|
return nil, resp, err
|
||
|
}
|
||
|
|
||
|
doc := responseInfo.Results[0].Document
|
||
|
item := DriveItem{
|
||
|
Drivewsid: "FILE::com.apple.CloudDocs::" + doc.DocumentID,
|
||
|
Docwsid: doc.DocumentID,
|
||
|
Itemid: doc.ItemID,
|
||
|
Etag: doc.Etag,
|
||
|
ParentID: doc.ParentID,
|
||
|
DateModified: time.Unix(r.Mtime, 0),
|
||
|
DateCreated: time.Unix(r.Mtime, 0),
|
||
|
Type: doc.Type,
|
||
|
Name: doc.Name,
|
||
|
Size: doc.Size,
|
||
|
}
|
||
|
|
||
|
return &item, resp, err
|
||
|
}
|
||
|
|
||
|
// UpdateFileInfo represents the information for an update to a file in the DriveService.
|
||
|
type UpdateFileInfo struct {
|
||
|
AllowConflict bool `json:"allow_conflict"`
|
||
|
Btime int64 `json:"btime"`
|
||
|
Command string `json:"command"`
|
||
|
CreateShortGUID bool `json:"create_short_guid"`
|
||
|
Data struct {
|
||
|
Receipt string `json:"receipt,omitempty"`
|
||
|
ReferenceSignature string `json:"reference_signature,omitempty"`
|
||
|
Signature string `json:"signature,omitempty"`
|
||
|
Size int64 `json:"size,omitempty"`
|
||
|
WrappingKey string `json:"wrapping_key,omitempty"`
|
||
|
} `json:"data,omitempty"`
|
||
|
DocumentID string `json:"document_id"`
|
||
|
FileFlags FileFlags `json:"file_flags"`
|
||
|
Mtime int64 `json:"mtime"`
|
||
|
Path struct {
|
||
|
Path string `json:"path"`
|
||
|
StartingDocumentID string `json:"starting_document_id"`
|
||
|
} `json:"path"`
|
||
|
}
|
||
|
|
||
|
// FileFlags defines the file flags for a document.
|
||
|
type FileFlags struct {
|
||
|
IsExecutable bool `json:"is_executable"`
|
||
|
IsHidden bool `json:"is_hidden"`
|
||
|
IsWritable bool `json:"is_writable"`
|
||
|
}
|
||
|
|
||
|
// NewUpdateFileInfo creates a new UpdateFileInfo object with default values.
|
||
|
//
|
||
|
// Returns an UpdateFileInfo object.
|
||
|
func NewUpdateFileInfo() UpdateFileInfo {
|
||
|
return UpdateFileInfo{
|
||
|
Command: "add_file",
|
||
|
CreateShortGUID: true,
|
||
|
AllowConflict: true,
|
||
|
FileFlags: FileFlags{
|
||
|
IsExecutable: true,
|
||
|
IsHidden: false,
|
||
|
IsWritable: false,
|
||
|
},
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// DriveItemRaw is a raw drive item.
|
||
|
// not suure what to call this but there seems to be a "unified" and non "unified" drive item response. This is the non unified.
|
||
|
type DriveItemRaw struct {
|
||
|
ItemID string `json:"item_id"`
|
||
|
ItemInfo *DriveItemRawInfo `json:"item_info"`
|
||
|
}
|
||
|
|
||
|
// SplitName splits the name of a DriveItemRaw into its name and extension.
|
||
|
//
|
||
|
// It returns the name and extension as separate strings. If the name ends with a dot,
|
||
|
// it means there is no extension, so an empty string is returned for the extension.
|
||
|
// If the name does not contain a dot, it means
|
||
|
func (d *DriveItemRaw) SplitName() (string, string) {
|
||
|
name := d.ItemInfo.Name
|
||
|
// ends with a dot, no extension
|
||
|
if strings.HasSuffix(name, ".") {
|
||
|
return name, ""
|
||
|
}
|
||
|
lastInd := strings.LastIndex(name, ".")
|
||
|
|
||
|
if lastInd == -1 {
|
||
|
return name, ""
|
||
|
}
|
||
|
return name[:lastInd], name[lastInd+1:]
|
||
|
}
|
||
|
|
||
|
// ModTime returns the modification time of the DriveItemRaw.
|
||
|
//
|
||
|
// It parses the ModifiedAt field of the ItemInfo struct and converts it to a time.Time value.
|
||
|
// If the parsing fails, it returns the zero value of time.Time.
|
||
|
// The returned time.Time value represents the modification time of the DriveItemRaw.
|
||
|
func (d *DriveItemRaw) ModTime() time.Time {
|
||
|
i, err := strconv.ParseInt(d.ItemInfo.ModifiedAt, 10, 64)
|
||
|
if err != nil {
|
||
|
return time.Time{}
|
||
|
}
|
||
|
return time.UnixMilli(i)
|
||
|
}
|
||
|
|
||
|
// CreatedTime returns the creation time of the DriveItemRaw.
|
||
|
//
|
||
|
// It parses the CreatedAt field of the ItemInfo struct and converts it to a time.Time value.
|
||
|
// If the parsing fails, it returns the zero value of time.Time.
|
||
|
// The returned time.Time
|
||
|
func (d *DriveItemRaw) CreatedTime() time.Time {
|
||
|
i, err := strconv.ParseInt(d.ItemInfo.CreatedAt, 10, 64)
|
||
|
if err != nil {
|
||
|
return time.Time{}
|
||
|
}
|
||
|
return time.UnixMilli(i)
|
||
|
}
|
||
|
|
||
|
// DriveItemRawInfo is the raw information about a drive item.
|
||
|
type DriveItemRawInfo struct {
|
||
|
Name string `json:"name"`
|
||
|
// Extension is absolutely borked on endpoints so dont use it.
|
||
|
Extension string `json:"extension"`
|
||
|
Size int64 `json:"size,string"`
|
||
|
Type string `json:"type"`
|
||
|
Version string `json:"version"`
|
||
|
ModifiedAt string `json:"modified_at"`
|
||
|
CreatedAt string `json:"created_at"`
|
||
|
Urls struct {
|
||
|
URLDownload string `json:"url_download"`
|
||
|
} `json:"urls"`
|
||
|
}
|
||
|
|
||
|
// IntoDriveItem converts a DriveItemRaw into a DriveItem.
|
||
|
//
|
||
|
// It takes no parameters.
|
||
|
// It returns a pointer to a DriveItem.
|
||
|
func (d *DriveItemRaw) IntoDriveItem() *DriveItem {
|
||
|
name, extension := d.SplitName()
|
||
|
return &DriveItem{
|
||
|
Itemid: d.ItemID,
|
||
|
Name: name,
|
||
|
Extension: extension,
|
||
|
Type: d.ItemInfo.Type,
|
||
|
Etag: d.ItemInfo.Version,
|
||
|
DateModified: d.ModTime(),
|
||
|
DateCreated: d.CreatedTime(),
|
||
|
Size: d.ItemInfo.Size,
|
||
|
Urls: d.ItemInfo.Urls,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// DocumentUpdateResponse is the response of a document update request.
|
||
|
type DocumentUpdateResponse struct {
|
||
|
Status struct {
|
||
|
StatusCode int `json:"status_code"`
|
||
|
ErrorMessage string `json:"error_message"`
|
||
|
} `json:"status"`
|
||
|
Results []struct {
|
||
|
Status struct {
|
||
|
StatusCode int `json:"status_code"`
|
||
|
ErrorMessage string `json:"error_message"`
|
||
|
} `json:"status"`
|
||
|
OperationID interface{} `json:"operation_id"`
|
||
|
Document *Document `json:"document"`
|
||
|
} `json:"results"`
|
||
|
}
|
||
|
|
||
|
// Document represents a document on iCloud.
|
||
|
type Document struct {
|
||
|
Status struct {
|
||
|
StatusCode int `json:"status_code"`
|
||
|
ErrorMessage string `json:"error_message"`
|
||
|
} `json:"status"`
|
||
|
DocumentID string `json:"document_id"`
|
||
|
ItemID string `json:"item_id"`
|
||
|
Urls struct {
|
||
|
URLDownload string `json:"url_download"`
|
||
|
} `json:"urls"`
|
||
|
Etag string `json:"etag"`
|
||
|
ParentID string `json:"parent_id"`
|
||
|
Name string `json:"name"`
|
||
|
Type string `json:"type"`
|
||
|
Deleted bool `json:"deleted"`
|
||
|
Mtime int64 `json:"mtime"`
|
||
|
LastEditorName string `json:"last_editor_name"`
|
||
|
Data DocumentData `json:"data"`
|
||
|
Size int64 `json:"size"`
|
||
|
Btime int64 `json:"btime"`
|
||
|
Zone string `json:"zone"`
|
||
|
FileFlags struct {
|
||
|
IsExecutable bool `json:"is_executable"`
|
||
|
IsWritable bool `json:"is_writable"`
|
||
|
IsHidden bool `json:"is_hidden"`
|
||
|
} `json:"file_flags"`
|
||
|
LastOpenedTime int64 `json:"lastOpenedTime"`
|
||
|
RestorePath interface{} `json:"restorePath"`
|
||
|
HasChainedParent bool `json:"hasChainedParent"`
|
||
|
}
|
||
|
|
||
|
// DriveID returns the drive ID of the Document.
|
||
|
func (d *Document) DriveID() string {
|
||
|
if d.Zone == "" {
|
||
|
d.Zone = defaultZone
|
||
|
}
|
||
|
return d.Type + "::" + d.Zone + "::" + d.DocumentID
|
||
|
}
|
||
|
|
||
|
// DocumentData represents the data of a document.
|
||
|
type DocumentData struct {
|
||
|
Signature string `json:"signature"`
|
||
|
Owner string `json:"owner"`
|
||
|
Size int64 `json:"size"`
|
||
|
ReferenceSignature string `json:"reference_signature"`
|
||
|
WrappingKey string `json:"wrapping_key"`
|
||
|
PcsInfo string `json:"pcsInfo"`
|
||
|
}
|
||
|
|
||
|
// SingleFileResponse is the response of a single file request.
|
||
|
type SingleFileResponse struct {
|
||
|
SingleFile *SingleFileInfo `json:"singleFile"`
|
||
|
}
|
||
|
|
||
|
// SingleFileInfo represents the information of a single file.
|
||
|
type SingleFileInfo struct {
|
||
|
ReferenceSignature string `json:"referenceChecksum"`
|
||
|
Size int64 `json:"size"`
|
||
|
Signature string `json:"fileChecksum"`
|
||
|
WrappingKey string `json:"wrappingKey"`
|
||
|
Receipt string `json:"receipt"`
|
||
|
}
|
||
|
|
||
|
// UploadResponse is the response of an upload request.
|
||
|
type UploadResponse struct {
|
||
|
URL string `json:"url"`
|
||
|
DocumentID string `json:"document_id"`
|
||
|
}
|
||
|
|
||
|
// FileRequestToken represents the token of a file request.
|
||
|
type FileRequestToken struct {
|
||
|
URL string `json:"url"`
|
||
|
Token string `json:"token"`
|
||
|
Signature string `json:"signature"`
|
||
|
WrappingKey string `json:"wrapping_key"`
|
||
|
ReferenceSignature string `json:"reference_signature"`
|
||
|
}
|
||
|
|
||
|
// FileRequest represents the request of a file.
|
||
|
type FileRequest struct {
|
||
|
DocumentID string `json:"document_id"`
|
||
|
ItemID string `json:"item_id"`
|
||
|
OwnerDsid int64 `json:"owner_dsid"`
|
||
|
DataToken *FileRequestToken `json:"data_token,omitempty"`
|
||
|
PackageToken *FileRequestToken `json:"package_token,omitempty"`
|
||
|
DoubleEtag string `json:"double_etag"`
|
||
|
}
|
||
|
|
||
|
// CreateFoldersResponse is the response of a create folders request.
|
||
|
type CreateFoldersResponse struct {
|
||
|
Folders []*DriveItem `json:"folders"`
|
||
|
}
|
||
|
|
||
|
// DriveItem represents an item on iCloud.
|
||
|
type DriveItem struct {
|
||
|
DateCreated time.Time `json:"dateCreated"`
|
||
|
Drivewsid string `json:"drivewsid"`
|
||
|
Docwsid string `json:"docwsid"`
|
||
|
Itemid string `json:"item_id"`
|
||
|
Zone string `json:"zone"`
|
||
|
Name string `json:"name"`
|
||
|
ParentID string `json:"parentId"`
|
||
|
Hierarchy []DriveItem `json:"hierarchy"`
|
||
|
Etag string `json:"etag"`
|
||
|
Type string `json:"type"`
|
||
|
AssetQuota int64 `json:"assetQuota"`
|
||
|
FileCount int64 `json:"fileCount"`
|
||
|
ShareCount int64 `json:"shareCount"`
|
||
|
ShareAliasCount int64 `json:"shareAliasCount"`
|
||
|
DirectChildrenCount int64 `json:"directChildrenCount"`
|
||
|
Items []*DriveItem `json:"items"`
|
||
|
NumberOfItems int64 `json:"numberOfItems"`
|
||
|
Status string `json:"status"`
|
||
|
Extension string `json:"extension,omitempty"`
|
||
|
DateModified time.Time `json:"dateModified,omitempty"`
|
||
|
DateChanged time.Time `json:"dateChanged,omitempty"`
|
||
|
Size int64 `json:"size,omitempty"`
|
||
|
LastOpenTime time.Time `json:"lastOpenTime,omitempty"`
|
||
|
Urls struct {
|
||
|
URLDownload string `json:"url_download"`
|
||
|
} `json:"urls"`
|
||
|
}
|
||
|
|
||
|
// IsFolder returns true if the item is a folder.
|
||
|
func (d *DriveItem) IsFolder() bool {
|
||
|
return d.Type == "FOLDER" || d.Type == "APP_CONTAINER" || d.Type == "APP_LIBRARY"
|
||
|
}
|
||
|
|
||
|
// DownloadURL returns the download URL of the item.
|
||
|
func (d *DriveItem) DownloadURL() string {
|
||
|
return d.Urls.URLDownload
|
||
|
}
|
||
|
|
||
|
// FullName returns the full name of the item.
|
||
|
// name + extension
|
||
|
func (d *DriveItem) FullName() string {
|
||
|
if d.Extension != "" {
|
||
|
return d.Name + "." + d.Extension
|
||
|
}
|
||
|
return d.Name
|
||
|
}
|
||
|
|
||
|
// GetDocIDFromDriveID returns the DocumentID from the drive ID.
|
||
|
func GetDocIDFromDriveID(id string) string {
|
||
|
split := strings.Split(id, "::")
|
||
|
return split[len(split)-1]
|
||
|
}
|
||
|
|
||
|
// DeconstructDriveID returns the document type, zone, and document ID from the drive ID.
|
||
|
func DeconstructDriveID(id string) (docType, zone, docid string) {
|
||
|
split := strings.Split(id, "::")
|
||
|
if len(split) < 3 {
|
||
|
return "", "", id
|
||
|
}
|
||
|
return split[0], split[1], split[2]
|
||
|
}
|
||
|
|
||
|
// ConstructDriveID constructs a drive ID from the given components.
|
||
|
func ConstructDriveID(id string, zone string, t string) string {
|
||
|
return strings.Join([]string{t, zone, id}, "::")
|
||
|
}
|
||
|
|
||
|
// GetContentTypeForFile detects content type for given file name.
|
||
|
func GetContentTypeForFile(name string) string {
|
||
|
// detect MIME type by looking at the filename only
|
||
|
mimeType := mime.TypeByExtension(filepath.Ext(name))
|
||
|
if mimeType == "" {
|
||
|
// api requires a mime type passed in
|
||
|
mimeType = "text/plain"
|
||
|
}
|
||
|
return strings.Split(mimeType, ";")[0]
|
||
|
}
|