Skip to content

Commit be8cd46

Browse files
committed
feat(graph): add the driveItem versions API
Implements the MS Graph driveItemVersion operations on files: - GET .../items/{id}/versions lists the versions, newest first - GET .../versions/{version-id} returns one version, "current" describes the file itself - GET .../versions/{version-id}/content redirects to a signed download url for the version - POST .../versions/{version-id}/restoreVersion makes the version the current content The operations map onto ListFileVersions and RestoreFileVersion of the gateway. Version downloads point at the WebDAV meta endpoint, signed the same way as the driveItem download url. The download url is only added when requested via $select. The driveItemVersion models are vendored from the regenerated client of opencloud-eu/libre-graph-api#73 until that spec change is merged and the dependency can be bumped.
1 parent 5807eea commit be8cd46

5 files changed

Lines changed: 1063 additions & 0 deletions

File tree

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
package svc
2+
3+
import (
4+
"net/http"
5+
"net/url"
6+
"path"
7+
"sort"
8+
"time"
9+
10+
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
11+
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
12+
"github.com/go-chi/chi/v5"
13+
"github.com/go-chi/render"
14+
libregraph "github.com/opencloud-eu/libre-graph-api-go"
15+
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
16+
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
17+
18+
"github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode"
19+
)
20+
21+
const _versionIDCurrent = "current"
22+
23+
// ListDriveItemVersions lists the versions of a file
24+
func (g Graph) ListDriveItemVersions(w http.ResponseWriter, r *http.Request) {
25+
g.logger.Info().Msg("Calling ListDriveItemVersions")
26+
27+
itemID, ok := g.fileDriveItemFromRequest(w, r)
28+
if !ok {
29+
return
30+
}
31+
32+
versions, ok := g.listDriveItemVersions(w, r, itemID)
33+
if !ok {
34+
return
35+
}
36+
37+
render.Status(r, http.StatusOK)
38+
render.JSON(w, r, &ListResponse{Value: versions})
39+
}
40+
41+
// GetDriveItemVersion returns a single version of a file
42+
func (g Graph) GetDriveItemVersion(w http.ResponseWriter, r *http.Request) {
43+
g.logger.Info().Msg("Calling GetDriveItemVersion")
44+
45+
itemID, ok := g.fileDriveItemFromRequest(w, r)
46+
if !ok {
47+
return
48+
}
49+
versionID := chi.URLParam(r, "versionID")
50+
51+
if versionID == _versionIDCurrent {
52+
version, ok := g.currentDriveItemVersion(w, r, itemID)
53+
if !ok {
54+
return
55+
}
56+
render.Status(r, http.StatusOK)
57+
render.JSON(w, r, version)
58+
return
59+
}
60+
61+
version, ok := g.getDriveItemVersion(w, r, itemID, versionID)
62+
if !ok {
63+
return
64+
}
65+
66+
render.Status(r, http.StatusOK)
67+
render.JSON(w, r, version)
68+
}
69+
70+
// GetDriveItemVersionContent redirects to a signed download url for a version
71+
func (g Graph) GetDriveItemVersionContent(w http.ResponseWriter, r *http.Request) {
72+
g.logger.Info().Msg("Calling GetDriveItemVersionContent")
73+
74+
itemID, ok := g.fileDriveItemFromRequest(w, r)
75+
if !ok {
76+
return
77+
}
78+
versionID := chi.URLParam(r, "versionID")
79+
80+
user, ok := revactx.ContextGetUser(r.Context())
81+
if !ok {
82+
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "user not in context")
83+
return
84+
}
85+
86+
var downloadURL string
87+
var err error
88+
switch versionID {
89+
case _versionIDCurrent:
90+
downloadURL, err = g.signedDownloadURL(itemID, user.GetId().GetOpaqueId())
91+
default:
92+
if _, ok := g.getDriveItemVersion(w, r, itemID, versionID); !ok {
93+
return
94+
}
95+
downloadURL, err = g.signedVersionDownloadURL(itemID, versionID, user.GetId().GetOpaqueId())
96+
}
97+
if err != nil {
98+
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
99+
return
100+
}
101+
102+
http.Redirect(w, r, downloadURL, http.StatusFound)
103+
}
104+
105+
// RestoreDriveItemVersion restores a version of a file
106+
func (g Graph) RestoreDriveItemVersion(w http.ResponseWriter, r *http.Request) {
107+
g.logger.Info().Msg("Calling RestoreDriveItemVersion")
108+
109+
itemID, ok := g.fileDriveItemFromRequest(w, r)
110+
if !ok {
111+
return
112+
}
113+
versionID := chi.URLParam(r, "versionID")
114+
115+
gatewayClient, err := g.gatewaySelector.Next()
116+
if err != nil {
117+
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
118+
return
119+
}
120+
res, err := gatewayClient.RestoreFileVersion(r.Context(), &storageprovider.RestoreFileVersionRequest{
121+
Ref: &storageprovider.Reference{ResourceId: itemID},
122+
Key: versionID,
123+
})
124+
if err := errorcode.FromCS3Status(res.GetStatus(), err); err != nil {
125+
errorcode.RenderError(w, r, err)
126+
return
127+
}
128+
129+
render.Status(r, http.StatusNoContent)
130+
render.NoContent(w, r)
131+
}
132+
133+
// fileDriveItemFromRequest parses and stats the item, rendering 404 unless it is a file
134+
func (g Graph) fileDriveItemFromRequest(w http.ResponseWriter, r *http.Request) (*storageprovider.ResourceId, bool) {
135+
driveID, err := parseIDParam(r, "driveID")
136+
if err != nil {
137+
errorcode.RenderError(w, r, err)
138+
return nil, false
139+
}
140+
itemID, err := parseIDParam(r, "driveItemID")
141+
if err != nil {
142+
errorcode.RenderError(w, r, err)
143+
return nil, false
144+
}
145+
if driveID.GetStorageId() != itemID.GetStorageId() || driveID.GetSpaceId() != itemID.GetSpaceId() {
146+
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "Item does not exist")
147+
return nil, false
148+
}
149+
150+
gatewayClient, err := g.gatewaySelector.Next()
151+
if err != nil {
152+
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
153+
return nil, false
154+
}
155+
res, err := gatewayClient.Stat(r.Context(), &storageprovider.StatRequest{Ref: &storageprovider.Reference{ResourceId: &itemID}})
156+
if !renderStatStatus(w, r, res, err) {
157+
return nil, false
158+
}
159+
if res.GetInfo().GetType() != storageprovider.ResourceType_RESOURCE_TYPE_FILE {
160+
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "Item is not a file")
161+
return nil, false
162+
}
163+
return &itemID, true
164+
}
165+
166+
// renderStatStatus renders the error of a failed stat
167+
func renderStatStatus(w http.ResponseWriter, r *http.Request, res *storageprovider.StatResponse, err error) bool {
168+
switch {
169+
case err != nil:
170+
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
171+
return false
172+
case res.GetStatus().GetCode() == cs3rpc.Code_CODE_OK:
173+
return true
174+
case res.GetStatus().GetCode() == cs3rpc.Code_CODE_NOT_FOUND:
175+
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, res.GetStatus().GetMessage())
176+
return false
177+
case res.GetStatus().GetCode() == cs3rpc.Code_CODE_PERMISSION_DENIED:
178+
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, res.GetStatus().GetMessage())
179+
return false
180+
case res.GetStatus().GetCode() == cs3rpc.Code_CODE_UNAUTHENTICATED:
181+
errorcode.Unauthenticated.Render(w, r, http.StatusUnauthorized, res.GetStatus().GetMessage())
182+
return false
183+
default:
184+
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.GetStatus().GetMessage())
185+
return false
186+
}
187+
}
188+
189+
func (g Graph) listDriveItemVersions(w http.ResponseWriter, r *http.Request, itemID *storageprovider.ResourceId) ([]libregraph.DriveItemVersion, bool) {
190+
gatewayClient, err := g.gatewaySelector.Next()
191+
if err != nil {
192+
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
193+
return nil, false
194+
}
195+
res, err := gatewayClient.ListFileVersions(r.Context(), &storageprovider.ListFileVersionsRequest{
196+
Ref: &storageprovider.Reference{ResourceId: itemID},
197+
})
198+
if err := errorcode.FromCS3Status(res.GetStatus(), err); err != nil {
199+
errorcode.RenderError(w, r, err)
200+
return nil, false
201+
}
202+
203+
fileVersions := res.GetVersions()
204+
// newest first, the storage does not guarantee an order
205+
sort.SliceStable(fileVersions, func(i, j int) bool {
206+
if fileVersions[i].GetMtime() != fileVersions[j].GetMtime() {
207+
return fileVersions[i].GetMtime() > fileVersions[j].GetMtime()
208+
}
209+
return fileVersions[i].GetKey() > fileVersions[j].GetKey()
210+
})
211+
212+
versions := make([]libregraph.DriveItemVersion, 0, len(fileVersions))
213+
for _, fv := range fileVersions {
214+
version := libregraph.NewDriveItemVersion()
215+
version.SetId(fv.GetKey())
216+
version.SetLastModifiedDateTime(time.Unix(int64(fv.GetMtime()), 0).UTC())
217+
version.SetSize(int64(fv.GetSize()))
218+
g.setDriveItemVersionDownloadURL(r, version, itemID, fv.GetKey())
219+
versions = append(versions, *version)
220+
}
221+
return versions, true
222+
}
223+
224+
func (g Graph) getDriveItemVersion(w http.ResponseWriter, r *http.Request, itemID *storageprovider.ResourceId, versionID string) (*libregraph.DriveItemVersion, bool) {
225+
versions, ok := g.listDriveItemVersions(w, r, itemID)
226+
if !ok {
227+
return nil, false
228+
}
229+
for i := range versions {
230+
if versions[i].GetId() == versionID {
231+
return &versions[i], true
232+
}
233+
}
234+
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "Version does not exist")
235+
return nil, false
236+
}
237+
238+
// currentDriveItemVersion describes the file itself as a version
239+
func (g Graph) currentDriveItemVersion(w http.ResponseWriter, r *http.Request, itemID *storageprovider.ResourceId) (*libregraph.DriveItemVersion, bool) {
240+
gatewayClient, err := g.gatewaySelector.Next()
241+
if err != nil {
242+
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
243+
return nil, false
244+
}
245+
res, err := gatewayClient.Stat(r.Context(), &storageprovider.StatRequest{Ref: &storageprovider.Reference{ResourceId: itemID}})
246+
if !renderStatStatus(w, r, res, err) {
247+
return nil, false
248+
}
249+
250+
version := libregraph.NewDriveItemVersion()
251+
version.SetId(_versionIDCurrent)
252+
version.SetLastModifiedDateTime(cs3TimestampToTime(res.GetInfo().GetMtime()).UTC())
253+
version.SetSize(int64(res.GetInfo().GetSize()))
254+
if user, ok := revactx.ContextGetUser(r.Context()); ok && g.downloadURLRequested(r) {
255+
if u, err := g.signedDownloadURL(itemID, user.GetId().GetOpaqueId()); err == nil {
256+
version.SetMicrosoftGraphDownloadUrl(u)
257+
}
258+
}
259+
return version, true
260+
}
261+
262+
func (g Graph) setDriveItemVersionDownloadURL(r *http.Request, version *libregraph.DriveItemVersion, itemID *storageprovider.ResourceId, key string) {
263+
if !g.downloadURLRequested(r) {
264+
return
265+
}
266+
user, ok := revactx.ContextGetUser(r.Context())
267+
if !ok {
268+
return
269+
}
270+
u, err := g.signedVersionDownloadURL(itemID, key, user.GetId().GetOpaqueId())
271+
if err != nil {
272+
return
273+
}
274+
version.SetMicrosoftGraphDownloadUrl(u)
275+
}
276+
277+
// signedVersionDownloadURL signs a download url for the WebDAV meta endpoint of a version
278+
func (g BaseGraphService) signedVersionDownloadURL(itemID *storageprovider.ResourceId, key, userID string) (string, error) {
279+
if g.downloadSigner == nil {
280+
return "", ErrDownloadURLSigningNotConfigured
281+
}
282+
u, err := g.getWebDavMetaURL()
283+
if err != nil {
284+
return "", err
285+
}
286+
u.Path = path.Join(u.Path, storagespace.FormatResourceID(itemID), "v", key)
287+
return g.downloadSigner.Sign(u.String(), userID, downloadURLTTL)
288+
}
289+
290+
func (g BaseGraphService) getWebDavMetaURL() (*url.URL, error) {
291+
u := *g.publicBaseURL
292+
u.Path = path.Join(u.Path, path.Dir(path.Clean(g.config.Spaces.WebDavPath)), "meta")
293+
return &u, nil
294+
}

0 commit comments

Comments
 (0)