Skip to content

Commit 9c20d96

Browse files
committed
-fuse mount script
-bug fixes
1 parent fc15f4c commit 9c20d96

File tree

6 files changed

+307
-11
lines changed

6 files changed

+307
-11
lines changed

integration/mount-fuse.py

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
import datetime;
2+
import errno;
3+
from functools import partial;
4+
from fusepy import FUSE, FuseOSError, Operations;
5+
import json;
6+
import os;
7+
import requests;
8+
from stat import S_IFDIR, S_IFREG;
9+
import sys;
10+
import yaml;
11+
12+
class FileAttributes:
13+
def __init__(this, json):
14+
this.blobId = json["blobId"];
15+
this.size = json["size"];
16+
this.creationTime = this._ConvertDate(json["creationTime"]);
17+
this.lastModifiedTime = this._ConvertDate(json["lastModifiedTime"]);
18+
this.lastAccessedTime = json["lastAccessedTime"] / 1000;
19+
20+
#Private methods
21+
def _ConvertDate(this, iso):
22+
dt = datetime.datetime.fromisoformat(iso);
23+
return dt.timestamp();
24+
25+
class ODFS:
26+
def __init__(this, odfsEndpoint, accessToken):
27+
this._odfsEndpoint = odfsEndpoint;
28+
this._accessToken = accessToken;
29+
30+
#Public methods
31+
def DownloadBlobPart(this, blobId, streamingKey, offset, length):
32+
start = str(offset);
33+
end = str(offset + length - 1);
34+
result = this._GetRaw("/stream?blobId=" + str(blobId) + "&streamingKey=" + streamingKey, "bytes=" + start + "-" + end);
35+
return result;
36+
37+
def CreateStreamingKey(this, fileId):
38+
result = this._Post("/files/" + str(fileId) + "/stream");
39+
return result["streamingKey"];
40+
41+
def RequestContainers(this):
42+
return this._Get("/containers");
43+
44+
def RequestDirContents(this, containerId, dirPath):
45+
containerId = str(containerId);
46+
content = this._Get("/containers/" + containerId + "/files?dirPath=" + dirPath);
47+
return content;
48+
49+
def RequestFileAttributes(this, fileId):
50+
result = this._Get("/files/" + str(fileId) + "/meta-file-manager");
51+
return FileAttributes(result);
52+
53+
#Private methods
54+
def _Get(this, url):
55+
response = requests.get(this._odfsEndpoint + url, headers={ "Authorization": "Bearer " + this._accessToken });
56+
return response.json();
57+
58+
def _GetRaw(this, url, rangeHeader):
59+
response = requests.get(this._odfsEndpoint + url, headers={ "Authorization": "Bearer " + this._accessToken, "Range": rangeHeader });
60+
return response.content;
61+
62+
def _Post(this, url):
63+
response = requests.post(this._odfsEndpoint + url, headers={ "Authorization": "Bearer " + this._accessToken });
64+
return response.json();
65+
66+
class CacheEntry:
67+
def __init__(this, odfs, isDir, requestChildren):
68+
this._odfs = odfs;
69+
this.children = None;
70+
this.isDir = isDir;
71+
this.fileId = None;
72+
this.fileAttributes = None;
73+
this._requestChildren = requestChildren;
74+
75+
#Public methods
76+
def GetAttributes(this):
77+
if(this.isDir):
78+
return dict(
79+
st_mode=(S_IFDIR | 0o555),
80+
st_ctime=0,
81+
st_mtime=0,
82+
st_atime=0,
83+
st_nlink=2 + len(this._GetChildren()),
84+
st_gid=0,
85+
st_uid=0,
86+
);
87+
88+
this.GetFileAttributes();
89+
90+
return dict(
91+
st_mode=(S_IFREG | 0o444),
92+
st_nlink=1,
93+
st_size=this.fileAttributes.size,
94+
st_ctime=this.fileAttributes.creationTime,
95+
st_mtime=this.fileAttributes.lastModifiedTime,
96+
st_atime=this.fileAttributes.lastAccessedTime,
97+
);
98+
99+
def GetFileAttributes(this):
100+
if(this.fileAttributes is None):
101+
this.fileAttributes = this._odfs.RequestFileAttributes(this.fileId);
102+
return this.fileAttributes;
103+
104+
#Private methods
105+
def _GetChildren(this):
106+
if(this.children is None):
107+
this._requestChildren();
108+
return this.children;
109+
110+
class ContainerCache:
111+
def __init__(this, odfs, id):
112+
this._odfs = odfs;
113+
this._id = id;
114+
this._pathCache = {};
115+
this._openFiles = {};
116+
117+
#Public methods
118+
def CloseFile(this, fh):
119+
del this._openFiles[fh];
120+
121+
def DownloadSlice(this, fh, offset, length):
122+
(streamingKey, blobId) = this._openFiles[fh]
123+
return this._odfs.DownloadBlobPart(blobId, streamingKey, offset, length);
124+
125+
def GetAttributes(this, path):
126+
this._EnsureParentIsCached(path);
127+
128+
ce = this._pathCache[path];
129+
if(ce is None):
130+
raise FuseOSError(errno.ENOENT);
131+
return ce.GetAttributes();
132+
133+
def ListDirectoryContents(this, path):
134+
if(path not in this._pathCache):
135+
this._CacheChildren(path);
136+
ce = this._pathCache[path];
137+
if(ce.children is None):
138+
this._CacheChildren(path);
139+
140+
return ce.children;
141+
142+
def OpenFile(this, path):
143+
this._EnsureParentIsCached(path);
144+
145+
ce = this._pathCache[path];
146+
streamingKey = this._odfs.CreateStreamingKey(ce.fileId);
147+
fh = hash(streamingKey) % sys.maxsize;
148+
this._openFiles[fh] = (streamingKey, ce.GetFileAttributes().blobId);
149+
150+
return fh;
151+
152+
#Private methods
153+
def _CacheChildren(this, path):
154+
results = this._odfs.RequestDirContents(this._id, path);
155+
children = [];
156+
157+
for d in results["dirs"]:
158+
children.append(d);
159+
this._EnsureIsInCache(os.path.join(path, d), True);
160+
161+
for f in results["files"]:
162+
children.append(os.path.basename(f["filePath"]));
163+
fce = this._EnsureIsInCache(os.path.join(f["filePath"]), False);
164+
fce.fileId = f["id"];
165+
166+
ce = this._EnsureIsInCache(path, True);
167+
ce.children = children;
168+
169+
def _EnsureIsInCache(this, path, isDir):
170+
if(path not in this._pathCache):
171+
this._pathCache[path] = CacheEntry(this._odfs, isDir, partial(this._CacheChildren, path));
172+
return this._pathCache[path];
173+
174+
def _EnsureParentIsCached(this, path):
175+
if(path not in this._pathCache):
176+
parent = os.path.dirname(path);
177+
this.ListDirectoryContents(parent);
178+
if(path not in this._pathCache):
179+
this._pathCache[path] = None;
180+
raise FuseOSError(errno.ENOENT);
181+
182+
183+
class ODFS_FUSE(Operations):
184+
def __init__(this, odfs):
185+
this._odfs = odfs;
186+
this._containers = None;
187+
188+
#Public methods
189+
def access(this, path, mode):
190+
if(mode & os.W_OK):
191+
raise FuseOSError(errno.EACCES);
192+
193+
def getattr(this, path, fh=None):
194+
if(path == "/"):
195+
return this._VirtualFolderStats();
196+
(container, containerPath) = this._SplitIntoContainerAndPath(path);
197+
198+
return container.GetAttributes(containerPath);
199+
200+
def open(this, path, flags):
201+
(container, containerPath) = this._SplitIntoContainerAndPath(path);
202+
return container.OpenFile(containerPath);
203+
204+
def read(this, path, length, offset, fh):
205+
(container, containerPath) = this._SplitIntoContainerAndPath(path);
206+
blob = container.DownloadSlice(fh, offset, length);
207+
return blob;
208+
209+
def readdir(this, path, fh):
210+
if(path == "/"):
211+
this._EnsureHaveContainers();
212+
return ['.', '..'] + list(this._containers.keys());
213+
(container, containerPath) = this._SplitIntoContainerAndPath(path);
214+
return ['.', '..'] + container.ListDirectoryContents(containerPath);
215+
216+
def release(this, path, fh):
217+
(container, containerPath) = this._SplitIntoContainerAndPath(path);
218+
container.CloseFile(fh);
219+
220+
def statfs(this, path):
221+
return dict(f_bsize=512, f_blocks=4096, f_bavail=2048);
222+
223+
#Private methods
224+
def _EnsureHaveContainers(this):
225+
if(this._containers is None):
226+
containers = this._odfs.RequestContainers();
227+
containersDict = {};
228+
for c in containers:
229+
containersDict[c["name"]] = ContainerCache(this._odfs, c["id"]);
230+
this._containers = containersDict;
231+
232+
def _SplitIntoContainerAndPath(this, path):
233+
parts = path.split("/");
234+
del parts[0];
235+
236+
this._EnsureHaveContainers();
237+
if(parts[0] not in this._containers):
238+
raise FuseOSError(errno.ENOENT);
239+
240+
container = this._containers[parts[0]];
241+
return (container, "/" + "/".join(parts[1:]));
242+
243+
def _VirtualFolderStats(this):
244+
return dict(
245+
st_mode=(S_IFDIR | 0o555),
246+
st_ctime=0,
247+
st_mtime=0,
248+
st_atime=0,
249+
st_nlink=2,
250+
st_gid=0,
251+
st_uid=0,
252+
);
253+
254+
def RequestAccessToken(url, client_id, client_secret, scope):
255+
response = requests.post(url, data={"grant_type": "client_credentials", "client_id": client_id, "client_secret": client_secret, "scope": scope }, headers={"Content-Type": "application/x-www-form-urlencoded"}, verify=False);
256+
response.raise_for_status();
257+
return response.json()["access_token"];
258+
259+
260+
with open(sys.argv[1], 'r') as file:
261+
config = yaml.safe_load(file);
262+
263+
accessToken = RequestAccessToken(config["tokenEndpoint"], config["clientId"], config["clientSecret"], "Files.Read");
264+
odfs = ODFS(config["odfsEndpoint"], accessToken);
265+
FUSE(ODFS_FUSE(odfs), sys.argv[2], nothreads=True, foreground=True); #debug=True
266+
267+
#requires: apt install python3-fusepy

portal/src/config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export const CONFIG_OIDC: OAuth2Config = {
2525
flow: "authorizationCode",
2626
authorizeEndpoint: process.env.OPENOBJECTSTORAGE_AUTH_ENDPOINT!,
2727
clientId: process.env.OPENOBJECTSTORAGE_CLIENTID!,
28+
endSessionEndpoint: process.env.OPENOBJECTSTORAGE_ENDSESSION_ENDPOINT!,
2829
redirectURI: process.env.OPENOBJECTSTORAGE_REDIRECTURI!,
29-
tokenEndpoint: process.env.OPENOBJECTSTORAGE_TOKEN_ENDPOINT!
30+
tokenEndpoint: process.env.OPENOBJECTSTORAGE_TOKEN_ENDPOINT!,
31+
postLogoutRedirectURI: process.env.OPENOBJECTSTORAGE_POSTLOGOUTREDIRECTURI!
3032
};

portal/src/routing.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
* along with this program. If not, see <http://www.gnu.org/licenses/>.
1717
* */
1818

19-
import { JSX_CreateElement, OAuth2Guard, Routes } from "acfrontend";
19+
import { JSX_CreateElement, OAuth2Guard, OAuth2LoginRedirectHandler, Routes } from "acfrontend";
2020
import { ListContainersComponent } from "./containers/ListContainersComponent";
2121
import { CreateContainersComponent } from "./containers/CreateContainersComponent";
2222
import { ContainerSelectionComponent } from "./file-explorer/ContainerSelectionComponent";
@@ -64,6 +64,7 @@ const settingsRoutes: Routes = [
6464
export const routes : Routes = [
6565
{ path: "accessdenied", component: <p>Access denied.</p> },
6666
{ path: "settings", component: <SettingsComponent />, children: settingsRoutes, guards: [ new OAuth2Guard({ config: CONFIG_OIDC, scopes: [SCOPE_ADMIN] }) ] },
67+
{ path: "oauth2loggedin", component: <OAuth2LoginRedirectHandler /> },
6768
{ path: "{containerId}", children: containerRoutes, guards: [ new OAuth2Guard({ config: CONFIG_OIDC, scopes: [SCOPE_FILES_READ] }) ] },
6869
{ path: "", component: <ContainerSelectionComponent />, guards: [ new OAuth2Guard({ config: CONFIG_OIDC, scopes: [SCOPE_FILES_READ] }) ] }
6970
];

service/src/api/files.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,24 @@ class _api_
121121
return this.fileDownloadService.DownloadBlob(rev!.blobId, accessToken.sub);
122122
}
123123

124+
@Get("meta-file-manager")
125+
public async RequestFileManagerMetaData(
126+
@Common fileMetaData: FileMetaData,
127+
)
128+
{
129+
const revs = await this.filesController.QueryRevisions(fileMetaData.id);
130+
const newestRev = revs[revs.length - 1];
131+
const size = await this.blobsController.QueryBlobSize(newestRev.blobId);
132+
133+
return {
134+
blobId: newestRev.blobId,
135+
creationTime: revs[0].creationTimestamp,
136+
lastAccessedTime: this.accessCounterService.FetchLastAccessTime(newestRev.blobId),
137+
lastModifiedTime: newestRev.creationTimestamp,
138+
size: size!,
139+
};
140+
}
141+
124142
@Get("meta")
125143
public async RequestInFileMetadata(
126144
@Common fileMetaData: FileMetaData,
@@ -191,6 +209,9 @@ class _api_
191209
const rev = await this.filesController.QueryNewestRevision(fileMetaData.id);
192210
const blobId = rev!.blobId;
193211

212+
const streamableBlobIds = options.Values().Map(x => x.blobId).ToArray();
213+
streamableBlobIds.push(blobId); //the newest revision can always be accepted to be streamed (i.e. binary stream instead of video stream)
214+
194215
const avData = await this.blobsController.QueryMetaData(blobId, "av");
195216
if((avData !== undefined) && this.ffprobeService.IsStreamable(fileMetaData.mediaType, JSON.parse(avData)))
196217
{
@@ -201,8 +222,7 @@ class _api_
201222
});
202223
}
203224

204-
const blobIds = options.Values().Map(x => x.blobId).ToArray();
205-
const streamingKey = this.streamingService.CreateStreamingKey(accessToken.sub, accessToken.exp, request.ip, blobIds);
225+
const streamingKey = this.streamingService.CreateStreamingKey(accessToken.sub, accessToken.exp, request.ip, streamableBlobIds);
206226

207227
return Of<StreamingRequestResultDTO>({
208228
streamingKey,

service/src/data-access/BlobsController.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,9 @@ export class BlobsController
120120
`;
121121
const conn = await this.dbConnMgr.CreateAnyConnectionQueryExecutor();
122122
const row = await conn.SelectOne(query, blobId);
123-
return row?.size as number | undefined;
123+
if(row === undefined)
124+
return undefined;
125+
return parseInt(row.size);
124126
}
125127

126128
public async QueryBlobStorageInfo(blobId: number)

service/src/services/AccessCounterService.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,6 @@ export class AccessCounterService
6464

6565
public FetchBlobAccessCounts(blobId: number): AccessStatistics
6666
{
67-
const lastAccessTime = (this.latest?.FetchLastAccessTime(blobId)
68-
|| this.currentYear?.FetchLastAccessTime(blobId)
69-
|| this.pastYears?.FetchLastAccessTime(blobId)
70-
) ?? 0;
71-
7267
const counts: AccessCounts = {
7368
nearPast: this.currentYear?.FetchAccessCounts(blobId) ?? 0,
7469
past: this.pastYears?.FetchAccessCounts(blobId) ?? 0,
@@ -77,7 +72,7 @@ export class AccessCounterService
7772

7873
return {
7974
...counts,
80-
lastAccessTime,
75+
lastAccessTime: this.FetchLastAccessTime(blobId),
8176
storageTier: this.ComputeStorageTier(counts)
8277
};
8378
}
@@ -95,6 +90,15 @@ export class AccessCounterService
9590
return this.Average(c1.concat(c2));
9691
}
9792

93+
public FetchLastAccessTime(blobId: number)
94+
{
95+
const lastAccessTime = (this.latest?.FetchLastAccessTime(blobId)
96+
|| this.currentYear?.FetchLastAccessTime(blobId)
97+
|| this.pastYears?.FetchLastAccessTime(blobId)
98+
) ?? 0;
99+
return lastAccessTime;
100+
}
101+
98102
//Private methods
99103
private Average(stats: AccessStatistics[]): AccessStatistics
100104
{

0 commit comments

Comments
 (0)