m365

package
v1.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 26, 2026 License: AGPL-3.0 Imports: 28 Imported by: 0

Documentation

Overview

Package m365 executes Microsoft 365 backup jobs on the agent: it talks to Microsoft Graph, keeps a per-unit index of what the previous snapshot held, presents each user, site or team as a virtual tree whose unchanged items are never fetched, and uploads one snapshot per unit through the shared backup engine.

Index

Constants

View Source
const (
	ArchiveUser  = "user" // mailbox/ + onedrive/
	ArchiveSite  = "site" // files/<library>/
	ArchiveTeam  = "team" // messages/, hosted/, files/<library>/
	ArchiveEntra = spec.ArchiveEntra
)

Archive names: one archive per unit snapshot

View Source
const IndexVersion = 1

IndexVersion is bumped when the index layout changes incompatibly (old files are then ignored and the unit is enumerated afresh)

Variables

This section is empty.

Functions

func BuildTree

func BuildTree(root string, entries []Entry, dirs []Dir, now time.Time) (*scan.Node, error)

BuildTree assembles the virtual directory named root from entries and explicit directories. Parents are created as needed; files sort with the directory (gopbs sorts children again). A path component that is not a valid archive name is rewritten by safeComponent.

func IsResync

func IsResync(err error) bool

IsResync reports a delta link Graph no longer honours (410 Gone, resyncRequired)

func IsStatus

func IsStatus(err error, statuses ...int) bool

IsStatus reports whether err is a GraphError with one of the statuses

func List

func List[T any](ctx context.Context, c *Client, path string) ([]T, error)

List decodes every element of a collection into a slice

func Run

Run executes one Microsoft 365 backup: precheck → select units → one snapshot per unit (Concurrency in flight) → the directory snapshot with the run manifest

Types

type BrowseEntry

type BrowseEntry struct {
	Subject        string `json:"subject"`
	From           string `json:"from,omitempty"`
	Received       string `json:"received,omitempty"`
	HasAttachments bool   `json:"has_attachments,omitempty"`
}

BrowseEntry is the browsing index line of a message

type ChannelIndex

type ChannelIndex struct {
	Name string `json:"name"`
	// Months: "2026-10" → message id → message JSON as Graph returned it
	Months map[string]map[string]json.RawMessage `json:"months"`
}

ChannelIndex groups a channel's messages by month of creation

type Client

type Client struct {

	// Stats counts what the client did (read after the run)
	Stats Stats
	// contains filtered or unexported fields
}

Client is a Graph HTTP client with bearer auth, paging and throttling retries

func NewClient

func NewClient(ctx context.Context, creds spec.Credentials, authority, graphURL string) (*Client, error)

NewClient builds a client from the tenant credentials. graphURL "" means GraphURL; authority "" means AuthorityURL.

func (*Client) Download

func (c *Client) Download(ctx context.Context, url string) (io.ReadCloser, int64, error)

Download streams a content URL. Graph content endpoints answer with a redirect to a pre-authenticated URL on another host; the bearer header must not follow it there, which Go's client already guarantees for cross-host redirects. Returns the body and the Content-Length (-1 when unknown).

func (*Client) Each

func (c *Client) Each(ctx context.Context, path string, headers map[string]string, fn func(raw json.RawMessage) error) (deltaLink string, err error)

Each walks a collection page by page (nextLink) calling fn for every element; it returns the deltaLink of the last page (delta queries) or ""

func (*Client) Get

func (c *Client) Get(ctx context.Context, path string, out any, headers map[string]string) error

Get fetches one JSON document

func (*Client) Post

func (c *Client) Post(ctx context.Context, path string, body, out any) error

Post sends a JSON body and decodes the JSON answer

func (*Client) ReadAll

func (c *Client) ReadAll(ctx context.Context, url string, limit int64) ([]byte, error)

ReadAll downloads a content URL into memory (bounded)

func (*Client) URL

func (c *Client) URL(path string) string

URL joins a path onto the API base (absolute URLs pass through) and percent-encodes the query string (OData filters carry spaces and quotes)

type Dir

type Dir struct {
	Path  string
	Mtime time.Time
}

Dir is an explicit directory (empty folders; every parent of an entry is implied)

type DriveEntry

type DriveEntry struct {
	Path  string `json:"path"`
	Dir   bool   `json:"dir,omitempty"`
	Size  int64  `json:"size"`
	Mtime int64  `json:"mtime"`
	CTag  string `json:"ctag,omitempty"`
	// Metadata for the .fallbck/items.json sidecar
	WebURL     string `json:"web_url,omitempty"`
	CreatedBy  string `json:"created_by,omitempty"`
	ModifiedBy string `json:"modified_by,omitempty"`
}

DriveEntry is one drive item (file or folder)

type DriveIndex

type DriveIndex struct {
	ID     string `json:"id"`
	Name   string `json:"name"`
	WebURL string `json:"web_url,omitempty"`
	// Prefix is the archive directory the drive's items live under (onedrive, or
	// files/<library>)
	Prefix string                 `json:"prefix"`
	Delta  string                 `json:"delta,omitempty"`
	Items  map[string]*DriveEntry `json:"items"`
}

DriveIndex is one OneDrive or document library

type Entry

type Entry struct {
	// Path is the archive path with '/' separators, no leading slash
	Path  string
	Size  int64
	Mtime time.Time
	Open  func() (io.ReadCloser, error)
}

Entry is one file of a unit's virtual tree. Open is called at most once, when gopbs reads the content; it is never called for an item reused from the previous snapshot.

type FolderIndex

type FolderIndex struct {
	ID       string `json:"id"`
	Name     string `json:"name"`
	ParentID string `json:"parent_id,omitempty"`
	Class    string `json:"class,omitempty"`
	// Path is the folder's archive path below mailbox/ (computed from the parents)
	Path       string                `json:"path"`
	ItemsDelta string                `json:"items_delta,omitempty"`
	Items      map[string]*ItemEntry `json:"items"`
	// Browse is the human-readable index of the folder (subject, from, dates) kept
	// through the standard messages delta; BrowseDelta its link
	BrowseDelta string                  `json:"browse_delta,omitempty"`
	Browse      map[string]*BrowseEntry `json:"browse,omitempty"`
}

FolderIndex is one mail folder with its items

type GraphError

type GraphError struct {
	Status  int
	Code    string
	Message string
	URL     string
	// contains filtered or unexported fields
}

GraphError is a non-2xx answer from Graph

func (*GraphError) Error

func (e *GraphError) Error() string

type HostedEntry

type HostedEntry struct {
	URL   string `json:"url"`
	Size  int64  `json:"size"`
	Mtime int64  `json:"mtime"`
}

HostedEntry is one hosted content blob of a message

type ItemEntry

type ItemEntry struct {
	// Size is the export stream size (what the archive declares); -1 until exported
	Size      int64  `json:"size"`
	ChangeKey string `json:"change_key"`
	// Mtime is lastModifiedDateTime (unix seconds); Created is createdDateTime, used
	// in the file name
	Mtime   int64  `json:"mtime"`
	Created int64  `json:"created"`
	Class   string `json:"class"`
	// EstSize is the mailboxItem.size Graph reported (packing estimate for exports)
	EstSize int64 `json:"est_size,omitempty"`
	// Dirty: the item must be exported again (new, changed or a failed export)
	Dirty bool `json:"dirty,omitempty"`
}

ItemEntry is one exported item as the previous snapshot holds it

type MailboxIndex

type MailboxIndex struct {
	MailboxID    string                  `json:"mailbox_id"`
	FoldersDelta string                  `json:"folders_delta,omitempty"`
	Folders      map[string]*FolderIndex `json:"folders"`
}

MailboxIndex is the mailbox of a user unit

type Page

type Page struct {
	Value     []json.RawMessage `json:"value"`
	NextLink  string            `json:"@odata.nextLink"`
	DeltaLink string            `json:"@odata.deltaLink"`
}

Page is one page of a collection

type Principal

type Principal struct {
	ID          string `json:"id"`
	UPN         string `json:"userPrincipalName"`
	DisplayName string `json:"displayName"`
	Mail        string `json:"mail"`
	UserType    string `json:"userType"`
	Enabled     bool   `json:"accountEnabled"`
	Licenses    []struct {
		SKU string `json:"skuId"`
	} `json:"assignedLicenses"`
}

Principal is a user of the tenant as the job sees it

func SelectUsers

func SelectUsers(ctx context.Context, c *Client, sel spec.UserSelection) ([]Principal, []string, error)

SelectUsers lists the tenant's users and applies the job's selection: members of the named groups (or everyone), licensed accounts plus shared mailboxes (accounts with a mail address but no licence) unless Unlicensed asks for every account, never guests, then the UPN include/exclude globs

func (Principal) Licensed

func (p Principal) Licensed() bool

Licensed reports whether the account holds a licence

type RunOptions

type RunOptions struct {
	Session     *temporal.BackupSessionInfo
	Spec        *spec.Spec
	Credentials spec.Credentials
	// StateDir holds the job's unit indexes (<data>/m365-state/<job id>)
	StateDir string
	// SpoolDir receives exported mailbox items while a unit uploads (<spool>/<run id>)
	SpoolDir string
	Upload   UploadFunc
	Progress func(phase string)
	Items    func(items map[string]rundetail.ProgressItem)
	Hostname string
	Version  string
	// Client overrides the Graph client (tests); Authority / GraphURL point at a fake
	Client    *Client
	Authority string
	GraphURL  string
	Now       func() time.Time
}

RunOptions parameterise Run

type Site

type Site struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	DisplayName string `json:"displayName"`
	WebURL      string `json:"webUrl"`
}

Site is a SharePoint site

func SelectSites

func SelectSites(ctx context.Context, c *Client, sel spec.Selection, teamSites map[string]bool) ([]Site, error)

SelectSites lists every SharePoint site and keeps those the selection names, minus the sites that belong to teams (they are archived with their team)

type State

type State struct{ Dir string }

State is the per-job index directory (<data>/m365-state/<job id>)

func (State) Load

func (s State) Load(group string) (*UnitIndex, error)

Load returns the unit's index, or an empty one when none (or an older version) exists

func (State) Save

func (s State) Save(idx *UnitIndex) error

Save writes the index atomically

type Stats

type Stats struct {
	Requests   atomic.Int64
	Retries    atomic.Int64
	Downloaded atomic.Uint64
}

Stats are the client's request counters; units run in parallel, so they are atomic

type Team

type Team struct {
	ID          string `json:"id"`
	DisplayName string `json:"displayName"`
}

Team is a Microsoft 365 group that is a team

func SelectTeams

func SelectTeams(ctx context.Context, c *Client, sel spec.Selection) ([]Team, error)

SelectTeams lists the groups that are teams and keeps those the selection names

type TeamsIndex

type TeamsIndex struct {
	// Since is the newest lastModifiedDateTime seen (the next run filters from here)
	Since    string                   `json:"since,omitempty"`
	Channels map[string]*ChannelIndex `json:"channels"`
	// Hosted are inline images and files by archive path (hosted/<message>-<id>)
	Hosted map[string]*HostedEntry `json:"hosted,omitempty"`
}

TeamsIndex holds a team's channel messages (the backup data itself: messages are small and must be re-emitted unchanged for chunk reuse)

type Unit

type Unit struct {
	Kind     string
	Name     string
	ObjectID string
	Group    string
	// Archive is the pxar name of the unit
	Archive string
	// Principal / Site / Team carry the Graph object
	Principal *Principal
	Site      *Site
	Team      *Team
}

Unit is one snapshot of the run: a user, a site, a team or the directory

type UnitIndex

type UnitIndex struct {
	Version  int    `json:"version"`
	Kind     string `json:"kind"`
	Name     string `json:"name"`
	ObjectID string `json:"object_id"`
	Group    string `json:"group"`
	// LastRunID / LastStatus / LastSnapshot describe the newest finished snapshot; an
	// activity retry skips units whose LastRunID is the current run
	LastRunID    string `json:"last_run_id,omitempty"`
	LastStatus   string `json:"last_status,omitempty"`
	LastSnapshot int64  `json:"last_snapshot,omitempty"`

	Mailbox *MailboxIndex          `json:"mailbox,omitempty"`
	Drives  map[string]*DriveIndex `json:"drives,omitempty"`
	Teams   *TeamsIndex            `json:"teams,omitempty"`
}

UnitIndex is what the agent remembers about one unit (user, site, team or the directory) between runs: the Graph delta links and, per item, the metadata the previous snapshot recorded. Saved only after the unit's snapshot finished, so a failed run repeats the same delta round instead of skipping anything.

type UploadFunc

type UploadFunc func(ctx context.Context, unit *Unit, tree *scan.Node, blobs []gopbs.Blob) (*temporal.BackupExecutionResult, error)

UploadFunc writes one unit snapshot: the caller mints the unit's ticket, opens the transport and runs the backup engine with the tree and the blobs

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL