Reconciliation-Store, Graph-Kind-Element-Adapter und Sync-Job-Runner fuer den SharePoint/OneDrive-Dokumentabgleich (M365-02), aufbauend auf m365connector (M365-01) und dem syncjob-Muster aus masterdatasync (ERP-03). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
96 lines
2.9 KiB
Go
96 lines
2.9 KiB
Go
package m365docsync
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/connect/internal/credential"
|
|
)
|
|
|
|
// GraphReader is the minimal capability GraphSource needs from
|
|
// m365connector.Client: an authenticated raw Graph GET. Kept as an
|
|
// interface (rather than importing *m365connector.Client directly) so the
|
|
// dependency stays the same direction as erpexport -> erpconnector.
|
|
type GraphReader interface {
|
|
Read(ctx context.Context, secret credential.Secret, path string) ([]byte, error)
|
|
}
|
|
|
|
// GraphSource lists the documents of one SharePoint/OneDrive drive folder
|
|
// via Microsoft Graph's drive-item "children" listing, and adapts them to
|
|
// ExternalDocument for Reconcile.
|
|
type GraphSource struct {
|
|
Graph GraphReader
|
|
Secret credential.Secret
|
|
DriveID string
|
|
ItemPath string
|
|
}
|
|
|
|
func (g GraphSource) childrenPath() string {
|
|
if g.ItemPath == "" || g.ItemPath == "/" {
|
|
return "/drives/" + g.DriveID + "/root/children"
|
|
}
|
|
return "/drives/" + g.DriveID + "/root:" + g.ItemPath + ":/children"
|
|
}
|
|
|
|
type graphDriveItemList struct {
|
|
Value []graphDriveItem `json:"value"`
|
|
}
|
|
|
|
type graphDriveItem struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Size int64 `json:"size"`
|
|
LastModifiedDateTime string `json:"lastModifiedDateTime"`
|
|
ParentReference struct {
|
|
Path string `json:"path"`
|
|
} `json:"parentReference"`
|
|
File *struct {
|
|
Hashes struct {
|
|
QuickXorHash string `json:"quickXorHash"`
|
|
} `json:"hashes"`
|
|
} `json:"file"`
|
|
Folder *struct{} `json:"folder"`
|
|
}
|
|
|
|
// ListDocuments implements Source. Folders in the listing are skipped --
|
|
// only actual drive items with a "file" facet are synced as documents.
|
|
func (g GraphSource) ListDocuments(ctx context.Context) ([]ExternalDocument, error) {
|
|
body, err := g.Graph.Read(ctx, g.Secret, g.childrenPath())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("m365docsync: list documents: %w", err)
|
|
}
|
|
var list graphDriveItemList
|
|
if err := json.Unmarshal(body, &list); err != nil {
|
|
return nil, fmt.Errorf("m365docsync: list documents: decode: %w", err)
|
|
}
|
|
|
|
out := make([]ExternalDocument, 0, len(list.Value))
|
|
for _, item := range list.Value {
|
|
if item.File == nil {
|
|
continue
|
|
}
|
|
modifiedAt, err := time.Parse(time.RFC3339, item.LastModifiedDateTime)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("m365docsync: list documents: parse lastModifiedDateTime for %s: %w", item.ID, err)
|
|
}
|
|
hash := item.File.Hashes.QuickXorHash
|
|
if hash == "" {
|
|
// Fallback for drive items Graph did not compute a hash for
|
|
// yet -- size+modification time is still enough to detect a
|
|
// change, just less precise than a content hash.
|
|
hash = fmt.Sprintf("%d-%s", item.Size, item.LastModifiedDateTime)
|
|
}
|
|
out = append(out, ExternalDocument{
|
|
ExternalID: item.ID,
|
|
Name: item.Name,
|
|
Path: item.ParentReference.Path,
|
|
SizeBytes: item.Size,
|
|
ModifiedAt: modifiedAt,
|
|
ContentHash: hash,
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|