- Added new functionality to resolve external URLs for files on Google Drive and Microsoft OneDrive mounts. - Introduced `mount_cloud_service.go` to handle OAuth token extraction and URL resolution. - Enhanced `mounts_service.go` to update mount configurations with OAuth tokens. - Updated API routes to include a new endpoint for fetching external URLs. - Implemented enrichment functions in `cloud_native.go` to mark files that should open in the provider's web editor. - Added tests for cloud-native file enrichment in `cloud_native_test.go` to ensure correct behavior.
195 lines
5.7 KiB
Go
195 lines
5.7 KiB
Go
package nextcloud
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// MountCloudResolver resolves provider web URLs for files on external mounts.
|
|
type MountCloudResolver struct {
|
|
Client *http.Client
|
|
}
|
|
|
|
func (r *MountCloudResolver) httpClient() *http.Client {
|
|
if r != nil && r.Client != nil {
|
|
return r.Client
|
|
}
|
|
return &http.Client{Timeout: 45 * time.Second}
|
|
}
|
|
|
|
// ParseOAuthAccessToken extracts a bearer token from Nextcloud external storage token JSON.
|
|
func ParseOAuthAccessToken(raw string) string {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return ""
|
|
}
|
|
var obj map[string]any
|
|
if err := json.Unmarshal([]byte(raw), &obj); err == nil {
|
|
if at, ok := obj["access_token"].(string); ok && strings.TrimSpace(at) != "" {
|
|
return strings.TrimSpace(at)
|
|
}
|
|
}
|
|
return raw
|
|
}
|
|
|
|
func (r *MountCloudResolver) GoogleDriveWebViewLink(ctx context.Context, accessToken, fileID string) (string, error) {
|
|
fileID = strings.TrimSpace(fileID)
|
|
accessToken = strings.TrimSpace(accessToken)
|
|
if fileID == "" || accessToken == "" {
|
|
return "", fmt.Errorf("missing google file id or token")
|
|
}
|
|
apiURL := "https://www.googleapis.com/drive/v3/files/" + url.PathEscape(fileID) +
|
|
"?fields=webViewLink,mimeType&supportsAllDrives=true"
|
|
body, err := r.providerGET(ctx, accessToken, apiURL)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
var parsed struct {
|
|
WebViewLink string `json:"webViewLink"`
|
|
MimeType string `json:"mimeType"`
|
|
}
|
|
if err := json.Unmarshal(body, &parsed); err != nil {
|
|
return "", err
|
|
}
|
|
if strings.TrimSpace(parsed.WebViewLink) != "" {
|
|
return strings.TrimSpace(parsed.WebViewLink), nil
|
|
}
|
|
// Fallback when API omits webViewLink but mime is native Google Workspace.
|
|
return driverootGoogleWorkspaceURL(parsed.MimeType, fileID), nil
|
|
}
|
|
|
|
func driverootGoogleWorkspaceURL(mimeType, fileID string) string {
|
|
switch strings.TrimSpace(mimeType) {
|
|
case "application/vnd.google-apps.document":
|
|
return "https://docs.google.com/document/d/" + fileID + "/edit"
|
|
case "application/vnd.google-apps.spreadsheet":
|
|
return "https://docs.google.com/spreadsheets/d/" + fileID + "/edit"
|
|
case "application/vnd.google-apps.presentation":
|
|
return "https://docs.google.com/presentation/d/" + fileID + "/edit"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (r *MountCloudResolver) ResolveGoogleDrivePath(ctx context.Context, accessToken, logicalPath string) (string, error) {
|
|
segments := splitLogicalPathSegments(logicalPath)
|
|
if len(segments) == 0 {
|
|
return "", fmt.Errorf("empty path")
|
|
}
|
|
parentID := "root"
|
|
for i, name := range segments {
|
|
isLast := i == len(segments)-1
|
|
id, mime, err := r.googleFindChild(ctx, accessToken, parentID, name, isLast)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if isLast {
|
|
link, err := r.GoogleDriveWebViewLink(ctx, accessToken, id)
|
|
if err != nil {
|
|
if built := driverootGoogleWorkspaceURL(mime, id); built != "" {
|
|
return built, nil
|
|
}
|
|
return "", err
|
|
}
|
|
return link, nil
|
|
}
|
|
parentID = id
|
|
}
|
|
return "", fmt.Errorf("path not found")
|
|
}
|
|
|
|
func (r *MountCloudResolver) googleFindChild(ctx context.Context, accessToken, parentID, name string, wantFile bool) (id, mimeType string, err error) {
|
|
q := fmt.Sprintf("'%s' in parents and name = '%s' and trashed = false", parentID, escapeDriveQuery(name))
|
|
if wantFile {
|
|
q += " and mimeType != 'application/vnd.google-apps.folder'"
|
|
} else {
|
|
q += " and mimeType = 'application/vnd.google-apps.folder'"
|
|
}
|
|
listURL := "https://www.googleapis.com/drive/v3/files?supportsAllDrives=true&includeItemsFromAllDrives=true&fields=files(id,mimeType)&q=" +
|
|
url.QueryEscape(q)
|
|
body, err := r.providerGET(ctx, accessToken, listURL)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
var parsed struct {
|
|
Files []struct {
|
|
ID string `json:"id"`
|
|
MimeType string `json:"mimeType"`
|
|
} `json:"files"`
|
|
}
|
|
if err := json.Unmarshal(body, &parsed); err != nil {
|
|
return "", "", err
|
|
}
|
|
if len(parsed.Files) == 0 {
|
|
return "", "", fmt.Errorf("google drive item not found: %s", name)
|
|
}
|
|
return parsed.Files[0].ID, parsed.Files[0].MimeType, nil
|
|
}
|
|
|
|
func escapeDriveQuery(s string) string {
|
|
return strings.ReplaceAll(s, "'", "\\'")
|
|
}
|
|
|
|
func (r *MountCloudResolver) ResolveMicrosoftDrivePath(ctx context.Context, accessToken, logicalPath string) (string, error) {
|
|
segments := splitLogicalPathSegments(logicalPath)
|
|
if len(segments) == 0 {
|
|
return "", fmt.Errorf("empty path")
|
|
}
|
|
encoded := make([]string, len(segments))
|
|
for i, seg := range segments {
|
|
encoded[i] = url.PathEscape(seg)
|
|
}
|
|
itemPath := "/me/drive/root:/" + strings.Join(encoded, "/")
|
|
apiURL := "https://graph.microsoft.com/v1.0" + itemPath + "?$select=webUrl"
|
|
body, err := r.providerGET(ctx, accessToken, apiURL)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
var parsed struct {
|
|
WebURL string `json:"webUrl"`
|
|
}
|
|
if err := json.Unmarshal(body, &parsed); err != nil {
|
|
return "", err
|
|
}
|
|
webURL := strings.TrimSpace(parsed.WebURL)
|
|
if webURL == "" {
|
|
return "", fmt.Errorf("microsoft drive item has no webUrl")
|
|
}
|
|
return webURL, nil
|
|
}
|
|
|
|
func splitLogicalPathSegments(logicalPath string) []string {
|
|
logicalPath = strings.Trim(logicalPath, "/")
|
|
if logicalPath == "" {
|
|
return nil
|
|
}
|
|
return strings.Split(logicalPath, "/")
|
|
}
|
|
|
|
func (r *MountCloudResolver) providerGET(ctx context.Context, accessToken, apiURL string) ([]byte, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
|
resp, err := r.httpClient().Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
body, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return nil, fmt.Errorf("provider api %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
|
}
|
|
return body, nil
|
|
}
|