docs, workflow change

docs were updated, and going to be doing all primary (V1+) work in master branch.
when ready for a release, i'll merge it into that particular branch.
This commit is contained in:
brent s. 2021-11-27 02:24:22 -05:00
parent dbc0962e46
commit a5b479ae4e
Signed by: bts
GPG Key ID: 8C004C2F93481F6B
12 changed files with 518 additions and 44 deletions

View File

@ -1,6 +1,5 @@
= libsecret/gosecret = libsecret/gosecret
Brent Saner <bts@square-r00t.net> Brent Saner <bts@square-r00t.net>
Last updated {localdatetime}
:doctype: book :doctype: book
:docinfo: shared :docinfo: shared
:data-uri: :data-uri:
@ -26,6 +25,7 @@ This project is originally forked from https://github.com/gsterjov/go-libsecret[
and as such, hopefully this library should serve as a more effective libsecret/SecretService interface. and as such, hopefully this library should serve as a more effective libsecret/SecretService interface.


== Backwards Compatability/Drop-In Replacement Support == Backwards Compatability/Drop-In Replacement Support

Version series `v0.X.X` of this library promises full and non-breaking backwards support of API interaction with the original project. The only changes should be internal optimizations, adding documentation, some file reorganizing, adding Golang module support, etc. -- all transparent from the library API itself. Version series `v0.X.X` of this library promises full and non-breaking backwards support of API interaction with the original project. The only changes should be internal optimizations, adding documentation, some file reorganizing, adding Golang module support, etc. -- all transparent from the library API itself.


To use this library as a replacement without significantly modifying your code, you can simply use a `replace` directive: To use this library as a replacement without significantly modifying your code, you can simply use a `replace` directive:
@ -43,6 +43,7 @@ replace (
and then run `go mod tidy`. and then run `go mod tidy`.


== New Developer API == New Developer API

Starting from `v1.0.0` onwards, entirely breaking changes can be assumed from the original project. Starting from `v1.0.0` onwards, entirely breaking changes can be assumed from the original project.


To use the new version, To use the new version,
@ -56,7 +57,70 @@ import (


To reflect the absolute breaking changes, the module name changes as well from `libsecret` to `gosecret`. To reflect the absolute breaking changes, the module name changes as well from `libsecret` to `gosecret`.


=== Status

The new API is underway, and all functionality in V0 is present. However, It's not "complete". https://github.com/johnnybubonic/gosecret/pulls[PRs^] welcome, of course, but this will be an ongoing effort for a bit of time.

== SecretService Concepts

For reference:

* A *`Service`* allows one to operate on/with *`Session`* objects.
* A *`Session`* allows one to operate on/with `*Collection*` objects.
* A `*Collection*` allows one to operate on/with `*Item*` objects.
* An `*Item*` allows one to operate on/with `*Secrets*`.
(`*Secrets*` are considered "terminating objects" in this model, and contain
actual secret value(s) and metadata).

Various interactions are handled by `*Prompts*`.

So the object hierarchy in *theory* looks kind of like this:

----
Service
├─ Session "A"
│ ├─ Collection "A.1"
│ │ ├─ Item "A.1.a"
│ │ │ ├─ Secret "A_1_a_I"
│ │ │ └─ Secret "A_1_a_II"
│ │ └─ Item "A.1.b"
│ │ ├─ Secret "A_1_b_I"
│ │ └─ Secret "A_1_b_II"
│ └─ Collection "A.2"
│ ├─ Item "A.2.a"
│ │ ├─ Secret "A_2_a_I"
│ │ └─ Secret "A_2_a_II"
│ └─ Item "A.2.b"
│ ├─ Secret "A_2_b_I"
│ └─ Secret "A_2_b_II"
└─ Session "B"
├─ Collection "B.1"
│ ├─ Item "B.1.a"
│ │ ├─ Secret "B_1_a_I"
│ │ └─ Secret "B_1_a_II"
│ └─ Item "B.1.b"
│ ├─ Secret "B_1_b_I"
│ └─ Secret "B_1_b_II"
└─ Collection "B.2"#
├─ Item "B.2.a"
│ ├─ Secret "B_2_a_I"
│ └─ Secret "B_2_a_II"
└─ Item "B.2.b"
├─ Secret "B_2_b_I"
└─ Secret "B_2_b_II"
----

And so on.

In *practice*, however, most users will only have two Session types:

* a default "system" one, and
* a temporary one that may or may not exist, running in memory for the current login session

and a single Collection, named `login` (and aliased to `default`, usually).

== Usage == Usage

Full documentation can be found via inline documentation. Either via the https://pkg.go.dev/r00t2.io/gosecret[pkg.go.dev documentation^] or https://pkg.go.dev/golang.org/x/tools/cmd/godoc[`godoc`^] (or `go doc`) in the source root. Full documentation can be found via inline documentation. Either via the https://pkg.go.dev/r00t2.io/gosecret[pkg.go.dev documentation^] or https://pkg.go.dev/golang.org/x/tools/cmd/godoc[`godoc`^] (or `go doc`) in the source root.


//// ////

View File

@ -1,43 +1,64 @@
package gosecret package gosecret


import ( import (
`fmt`
`path/filepath`
`strings`
`time`

`github.com/godbus/dbus` `github.com/godbus/dbus`
) )


// NewCollection returns a pointer to a new Collection based on a Dbus connection and a Dbus path. /*
func NewCollection(conn *dbus.Conn, path dbus.ObjectPath) (coll *Collection, err error) { CreateCollection creates a new Collection named `name` using the dbus.BusObject `secretServiceConn`.
`secretServiceConn` should be the same as used for Collection.Dbus (and/or NewCollection).
It will be called by NewCollection if the Collection does not exist in Dbus.


// dbus.Conn.Names() will ALWAYS return a []string with at least ONE element. Generally speaking, you should probably not use this function directly and instead use NewCollection.
if conn == nil || (conn.Names() == nil || len(conn.Names()) < 1) { */
func CreateCollection(secretServiceConn *dbus.BusObject, name string) (c *Collection, err error) {

var path dbus.ObjectPath

if secretServiceConn == nil {
err = ErrNoDbusConn err = ErrNoDbusConn
return return
} }


if path == "" { path = dbus.ObjectPath(strings.Join([]string{DbusPath, ""}, "/"))
err = ErrBadDbusPath
// TODO.

return
}

// NewCollection returns a pointer to a new Collection based on a Dbus connection and a Dbus path.
func NewCollection(conn *dbus.Conn, path dbus.ObjectPath) (coll *Collection, err error) {

if _, err = validConnPath(conn, path); err != nil {
return return
} }


coll = &Collection{ coll = &Collection{
Conn: conn, Conn: conn,
Dbus: conn.Object(DbusServiceName, path), Dbus: conn.Object(DbusService, path),
// lastModified: time.Now(),
} }


_, _, err = coll.Modified()

return return
} }


// Items returns a slice of Item pointers in the Collection. // Items returns a slice of Item pointers in the Collection.
func (c *Collection) Items() (items []*Item, err error) { func (c *Collection) Items() (items []*Item, err error) {


var variant dbus.Variant
var paths []dbus.ObjectPath var paths []dbus.ObjectPath


if variant, err = c.Dbus.GetProperty(DbusItemsID); err != nil { if paths, err = pathsFromPath(c.Dbus, DbusCollectionItems); err != nil {
return return
} }


paths = variant.Value().([]dbus.ObjectPath)

items = make([]*Item, len(paths)) items = make([]*Item, len(paths))


for idx, path := range paths { for idx, path := range paths {
@ -124,7 +145,7 @@ func (c *Collection) CreateItem(label string, secret *Secret, replace bool) (ite
return return
} }


// Locked indicates that a Collection is locked (true) or unlocked (false). // Locked indicates if a Collection is locked (true) or unlocked (false).
func (c *Collection) Locked() (isLocked bool, err error) { func (c *Collection) Locked() (isLocked bool, err error) {


var variant dbus.Variant var variant dbus.Variant
@ -138,3 +159,39 @@ func (c *Collection) Locked() (isLocked bool, err error) {


return return
} }

// Label returns the Collection label (name).
func (c *Collection) Label() (label string, err error) {

// TODO.

return
}

// Created returns the time.Time of when a Collection was created.
func (c *Collection) Created() (created time.Time, err error) {

// TODO.

return
}

/*
Modified returns the time.Time of when a Collection was last modified along with a boolean
that indicates if the collection has changed since the last call of Collection.Modified.

Note that when calling NewCollection, the internal library-tracked modification
time (Collection.lastModified) will be set to the modification time of the Collection
itself as reported by Dbus.
*/
func (c *Collection) Modified() (modified time.Time, isChanged bool, err error) {

// TODO.

if c.lastModified == time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC) {
// It's "nil", so set it to modified.
c.lastModified = modified
}

return
}

144
consts.go
View File

@ -1,19 +1,139 @@
package gosecret package gosecret


// Libsecret/SecretService/Dbus identifiers. // Libsecret/SecretService Dbus interfaces.
const ( const (
// DbusServiceName is the "root Dbus path" in identifier format. // DbusService is the Dbus service bus identifier.
DbusServiceName string = "org.freedesktop.secrets" DbusService string = "org.freedesktop.secrets"
// DbusItemsID is the Dbus identifier for Item. // DbusServiceBase is the base identifier used by interfaces.
DbusItemsID string = "org.freedesktop.Secret.Collection.Items" DbusServiceBase string = "org.freedesktop.Secret"
// DbusCollectionDelete is the Dbus identifier for Collection.Delete.
DbusCollectionDelete string = "org.freedesktop.Secret.Collection.Delete"
) )


// Dbus constants and paths. // Service interface.
const ( const (
// DbusPath is the path version of DbusServiceName. /*
DbusPath string = "/org/freedesktop/secrets" DbusInterfaceService is the Dbus interface for working with a Service.
// PromptPrefix is the path used for prompts comparison. Found at /org/freedesktop/secrets/(DbusInterfaceService)
PromptPrefix string = DbusPath + "/prompt/" */
DbusInterfaceService string = DbusServiceBase + ".Service"

// Methods

// DbusServiceChangeLock is [FUNCTION UNKNOWN; TODO.]
DbusServiceChangeLock string = DbusInterfaceService + ".ChangeLock"

// DbusServiceCreateCollection is used to create a new Collection if it doesn't exist in Dbus.
DbusServiceCreateCollection string = DbusInterfaceService + ".CreateCollection"

/*
DbusServiceGetSecrets is used to fetch all Secret / Item items in a given Collection
(via Service.GetSecrets).
*/
DbusServiceGetSecrets string = DbusInterfaceService + ".GetSecrets"

// DbusServiceLock is used by Service.Lock.
DbusServiceLock string = DbusInterfaceService + ".Lock"

// DbusServiceLockService is [FUNCTION UNKNOWN; TODO.]
DbusServiceLockService string = DbusInterfaceService + ".LockService"

// DbusServiceOpenSession is used by Service.Open.
DbusServiceOpenSession string = DbusInterfaceService + ".OpenSession"

// DbusServiceReadAlias is used by Service.GetAlias to return a Collection based on its aliased name.
DbusServiceReadAlias string = DbusInterfaceService + ".ReadAlias"

// DbusServiceSearchItems is used by Service.SearchItems to get arrays of locked and unlocked Item objects.
DbusServiceSearchItems string = DbusInterfaceService + ".SearchItems"

// DbusServiceSetAlias is used by Service.SetAlias to set an alias for a Collection.
DbusServiceSetAlias string = DbusInterfaceService + ".SetAlias"

// DbusServiceUnlock is used to unlock a Service.
DbusServiceUnlock string = DbusInterfaceService + ".Unlock"

// Properties

// DbusServiceCollections is used to get a Dbus array of Collection items.
DbusServiceCollections string = DbusInterfaceService + ".Collections"
)

// Session interface.
const (
/*
DbusInterfaceSession is the Dbus interface for working with a Session.
Found at /org/freedesktop/secrets/session/<session ID>/(DbusInterfaceSession)
*/
DbusInterfaceSession = DbusServiceBase + ".Session"

// Methods

// DbusSessionClose is used for Session.Close.
DbusSessionClose string = DbusInterfaceSession + ".Close"
)

// Collection interface.
const (
/*
DbusInterfaceCollection is the Dbus interface for working with a Collection.
Found at /org/freedesktop/secrets/collection/<collection name>/(DbusInterfaceCollection)
*/
DbusInterfaceCollection string = DbusServiceBase + ".Collection"

// Methods

// DbusCollectionCreateItem is used for Collection.CreateItem.
DbusCollectionCreateItem string = DbusInterfaceCollection + ".CreateItem"

// DbusCollectionDelete is used for Collection.Delete.
DbusCollectionDelete string = DbusInterfaceCollection + ".Delete"

// DbusCollectionSearchItems is used for Collection.SearchItems.
DbusCollectionSearchItems string = DbusInterfaceCollection + ".SearchItems"

// Properties

// DbusCollectionItems is a Dbus array of Item.
DbusCollectionItems string = DbusInterfaceCollection + ".Items"

// DbusCollectionLocked is a Dbus boolean for Collection.Locked.
DbusCollectionLocked string = DbusInterfaceCollection + ".Locked"

// DbusCollectionLabel is the name (label) for Collection.Label.
DbusCollectionLabel string = DbusInterfaceCollection + ".Label"

// DbusCollectionCreated is the time a Collection was created (in a UNIX Epoch uint64) for Collection.Created.
DbusCollectionCreated string = DbusInterfaceCollection + ".Created"

// DbusCollectionModified is the time a Collection was last modified (in a UNIX Epoch uint64) for Collection.Modified.
DbusCollectionModified string = DbusInterfaceCollection + ".Modified"

// TODO: Signals?
)

// Item interface.
const (
/*
DbusInterfaceItem is the Dbus interface for working with Item items.
Found at /org/freedesktop/secrets/collection/<collection name>/<item index>/(DbusInterfaceItem)
*/
DbusInterfaceItem string = DbusServiceBase + ".Item"

// Methods

// DbusItemDelete is used by Item.Delete.
DbusItemDelete string = DbusInterfaceItem + ".Delete"

// DbusItemGetSecret is used by Item.GetSecret.
DbusItemGetSecret string = DbusInterfaceItem + ".GetSecret"

// DbusItemSetSecret is used by Item.SetSecret.
DbusItemSetSecret string = DbusInterfaceItem + ".SetSecret"
)

// Dbus paths.
const (
// DbusPath is the path for DbusService.
DbusPath string = "/org/freedesktop/secrets"
// DbusPromptPrefix is the path used for prompts comparison.
DbusPromptPrefix string = DbusPath + "/prompt/"
) )

61
doc.go
View File

@ -10,11 +10,13 @@ As such, hopefully this library should serve as a more effective libsecret/Secre


Backwards Compatibility Backwards Compatibility


Version series `v0.X.X` of this library promises full and non-breaking backwards compatibility/drop-in support of API interaction with the original project. Version series `v0.X.X` of this library promises full and non-breaking backwards compatibility/drop-in
support of API interaction with the original project.
The only changes should be internal optimizations, adding documentation, some file reorganizing, adding Golang module support, The only changes should be internal optimizations, adding documentation, some file reorganizing, adding Golang module support,
etc. -- all transparent from the library API itself. etc. -- all transparent from the library API itself.


To use this library as a replacement without significantly modifying your code, you can simply use a `replace` directive in your go.mod file: To use this library as a replacement without significantly modifying your code,
you can simply use a `replace` directive in your go.mod file:


// ... // ...
replace ( replace (
@ -37,6 +39,61 @@ To use the new version,


To reflect the absolute breaking changes, the module name changes as well from `libsecret` to `gosecret`. To reflect the absolute breaking changes, the module name changes as well from `libsecret` to `gosecret`.


SecretService Concepts

For reference:

- A Service allows one to operate on/with Session objects.

- A Session allows one to operate on/with Collection objects.

- A Collection allows one to operate on/with Item objects.

- An Item allows one to operate on/with Secrets.

(Secrets are considered "terminating objects" in this model, and contain actual secret value(s) and metadata).

Various interactions are handled by Prompts.

So the object hierarchy in THEORY looks kind of like this:

Service
├─ Session "A"
│ ├─ Collection "A.1"
│ │ ├─ Item "A.1.a"
│ │ │ ├─ Secret "A_1_a_I"
│ │ │ └─ Secret "A_1_a_II"
│ │ └─ Item "A.1.b"
│ │ ├─ Secret "A_1_b_I"
│ │ └─ Secret "A_1_b_II"
│ └─ Collection "A.2"
│ ├─ Item "A.2.a"
│ │ ├─ Secret "A_2_a_I"
│ │ └─ Secret "A_2_a_II"
│ └─ Item "A.2.b"
│ ├─ Secret "A_2_b_I"
│ └─ Secret "A_2_b_II"
└─ Session "B"
├─ Collection "B.1"
│ ├─ Item "B.1.a"
│ │ ├─ Secret "B_1_a_I"
│ │ └─ Secret "B_1_a_II"
│ └─ Item "B.1.b"
│ ├─ Secret "B_1_b_I"
│ └─ Secret "B_1_b_II"
└─ Collection "B.2"#
├─ Item "B.2.a"
│ ├─ Secret "B_2_a_I"
│ └─ Secret "B_2_a_II"
└─ Item "B.2.b"
├─ Secret "B_2_b_I"
└─ Secret "B_2_b_II"

And so on.
In PRACTICE, however, most users will only have two Session types
(a default "system" one and a temporary one that may or may not exist, running in memory for the current login session)
and a single Collection, named "login" (and aliased to "default", usually).

Usage Usage


Full documentation can be found via inline documentation. Full documentation can be found via inline documentation.

View File

@ -4,7 +4,12 @@ import (
`errors` `errors`
) )


// Errors.
var ( var (
ErrNoDbusConn error = errors.New("no valid dbus connection") // ErrBadDbusPath indicates an invalid path - either nothing exists at that path or the path is malformed.
ErrBadDbusPath error = errors.New("invalid dbus path") ErrBadDbusPath error = errors.New("invalid dbus path")
// ErrInvalidProperty indicates a dbus.Variant is not the "real" type expected.
ErrInvalidProperty error = errors.New("invalid variant type; cannot convert")
// ErrNoDbusConn gets triggered if a connection to Dbus can't be detected.
ErrNoDbusConn error = errors.New("no valid dbus connection")
) )

103
funcs.go
View File

@ -9,8 +9,109 @@ import (
// isPrompt returns a boolean that is true if path is/requires a prompt(ed path) and false if it is/does not. // isPrompt returns a boolean that is true if path is/requires a prompt(ed path) and false if it is/does not.
func isPrompt(path dbus.ObjectPath) (prompt bool) { func isPrompt(path dbus.ObjectPath) (prompt bool) {


prompt = strings.HasPrefix(string(path), PromptPrefix) prompt = strings.HasPrefix(string(path), DbusPromptPrefix)


return return


} }

// connIsValid returns a boolean if the dbus.conn named conn is active.
func connIsValid(conn *dbus.Conn) (ok bool, err error) {

// dbus.Conn.Names() will ALWAYS return a []string with at least ONE element.
if conn == nil || (conn.Names() == nil || len(conn.Names()) < 1) {
err = ErrNoDbusConn
return
}

ok = true

return
}

/*
pathIsValid implements path checking for valid Dbus paths. Currently it only checks to make sure path is not a blank string.
The path argument can be either a string or dbus.ObjectPath.
*/
func pathIsValid(path interface{}) (ok bool, err error) {

var realPath string

switch p := path.(type) {
case dbus.ObjectPath:
if !p.IsValid() {
err = ErrBadDbusPath
return
}
realPath = string(p)
case string:
realPath = p
default:
err = ErrBadDbusPath
return
}

if strings.TrimSpace(realPath) == "" {
err = ErrBadDbusPath
return
}

ok = true

return
}

/*
validConnPath condenses the checks for connIsValid and pathIsValid into one func due to how frequently this check is done.
err is a MultiError, which can be treated as an error.error. (See https://pkg.go.dev/builtin#error)
*/
func validConnPath(conn *dbus.Conn, path interface{}) (cr *ConnPathCheckResult, err error) {

var connErr error
var pathErr error

cr = new(ConnPathCheckResult)

cr.ConnOK, connErr = connIsValid(conn)
cr.PathOK, pathErr = pathIsValid(path)

err = NewErrors(connErr, pathErr)

return
}

/*
pathsFromProp returns a slice of dbus.ObjectPath (paths) from a dbus.Variant (prop).
If prop cannot typeswitch to paths, an ErrInvalidProperty will be raised.
*/
func pathsFromProp(prop dbus.Variant) (paths []dbus.ObjectPath, err error) {

switch v := prop.Value().(type) {
case []dbus.ObjectPath:
paths = v
default:
err = ErrInvalidProperty
return
}

return
}

/*
pathsFromPath returns a slice of dbus.ObjectPath based on an object given by path using the dbus.Conn specified by conn.
Internally it uses pathsFromProp.
*/
func pathsFromPath(bus dbus.BusObject, path string) (paths []dbus.ObjectPath, err error) {

var v dbus.Variant

if v, err = bus.GetProperty(path); err != nil {
return
}

if paths, err = pathsFromProp(v); err != nil {
return
}

return
}

View File

@ -9,21 +9,12 @@ func NewItem(conn *dbus.Conn, path dbus.ObjectPath) (item *Item) {


item = &Item{ item = &Item{
Conn: conn, Conn: conn,
Dbus: conn.Object(DbusServiceName, path), Dbus: conn.Object(DbusService, path),
} }


return return
} }


// Path returns the path of the underlying Dbus connection.
func (i Item) Path() (path dbus.ObjectPath) {

// Remove this method in V1. It's bloat since we now have an exported Dbus.
path = i.Dbus.Path()

return
}

// Label returns the label ("name") of an Item. // Label returns the label ("name") of an Item.
func (i *Item) Label() (label string, err error) { func (i *Item) Label() (label string, err error) {



57
multierr_funcs.go Normal file
View File

@ -0,0 +1,57 @@
package gosecret

import (
`fmt`
)

/*
NewErrors returns a new MultiError based on a slice of error.Error (errs).
Any nil errors are trimmed. If there are no actual errors after trimming, err will be nil.
*/
func NewErrors(errs ...error) (err *MultiError) {

if errs == nil || len(errs) == 0 {
return
}

var realErrs []error = make([]error, 0)

for _, e := range errs {
if e == nil {
continue
}
realErrs = append(realErrs, e)
}

if len(realErrs) == 0 {
return
}

err = &MultiError{
Errors: realErrs,
ErrorSep: "\n",
}

return
}

func (e *MultiError) Error() (errStr string) {

var numErrs int

if e == nil || len(e.Errors) == 0 {
return
} else {
numErrs = len(e.Errors)
}

for idx, err := range e.Errors {
if (idx +1 ) < numErrs {
errStr += fmt.Sprintf(err.Error(), e.ErrorSep)
} else {
errStr += err.Error()
}
}

return
}

View File

@ -9,7 +9,7 @@ func NewPrompt(conn *dbus.Conn, path dbus.ObjectPath) (prompt *Prompt) {


prompt = &Prompt{ prompt = &Prompt{
Conn: conn, Conn: conn,
Dbus: conn.Object(DbusServiceName, path), Dbus: conn.Object(DbusService, path),
} }


return return

View File

@ -15,7 +15,7 @@ func NewService() (service *Service, err error) {
if service.Conn, err = dbus.SessionBus(); err != nil { if service.Conn, err = dbus.SessionBus(); err != nil {
return return
} }
service.Dbus = service.Conn.Object(DbusServiceName, dbus.ObjectPath(DbusPath)) service.Dbus = service.Conn.Object(DbusService, dbus.ObjectPath(DbusPath))


return return
} }

View File

@ -9,7 +9,7 @@ func NewSession(conn *dbus.Conn, path dbus.ObjectPath) (session *Session) {


session = &Session{ session = &Session{
Conn: conn, Conn: conn,
Dbus: conn.Object(DbusServiceName, path), Dbus: conn.Object(DbusService, path),
} }


return return

View File

@ -1,9 +1,29 @@
package gosecret package gosecret


import ( import (
`time`

`github.com/godbus/dbus` `github.com/godbus/dbus`
) )


/*
MultiError is a type of error.Error that can contain multiple error.Errors. Confused? Don't worry about it.
*/
type MultiError struct {
// Errors is a slice of errors to combine/concatenate when .Error() is called.
Errors []error
// ErrorSep is a string to use to separate errors for .Error(). The default is "\n".
ErrorSep string
}

// ConnPathCheckResult contains the result of validConnPath.
type ConnPathCheckResult struct {
// ConnOK is true if the dbus.Conn is valid.
ConnOK bool
// PathOK is true if the Dbus path given is a valid type and value.
PathOK bool
}

// DBusObject is any type that has a Path method that returns a dbus.ObjectPath. // DBusObject is any type that has a Path method that returns a dbus.ObjectPath.
type DBusObject interface { type DBusObject interface {
Path() dbus.ObjectPath Path() dbus.ObjectPath
@ -20,6 +40,8 @@ type Collection struct {
Conn *dbus.Conn Conn *dbus.Conn
// Dbus is the Dbus bus object. // Dbus is the Dbus bus object.
Dbus dbus.BusObject Dbus dbus.BusObject
// lastModified is unexported because it's important that API users don't change it; it's used by Collection.Modified.
lastModified time.Time
} }


/* /*