Files
nexarch/connect/internal/m365docsync/graphsource.go
patrickandClaude Sonnet 5 39bb4e0499 M365-02: 02 sharepoint onedrive dokumentabgleich
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>
2026-09-28 01:10:14 +00:00

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
}