Skip to content

Commit 71ca083

Browse files
adding appfolder uploadsession sample
1 parent c86587c commit 71ca083

File tree

11 files changed

+3811
-0
lines changed

11 files changed

+3811
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/build
2+
3+
node_modules/
4+
5+

samples/approot-uploadsession/package-lock.json

Lines changed: 3505 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "approot-uploadsession",
3+
"version": "0.0.1",
4+
"description": "Demonstrates how to create an upload session for the approot special folder",
5+
"main": "build/main.js",
6+
"type": "module",
7+
"scripts": {
8+
"test": "echo \"Error: no test specified\" && exit 1",
9+
"start": "tsc -p . && webpack --config ./build/webpack.config.js && node ./build/server.js"
10+
},
11+
"author": "",
12+
"license": "MIT",
13+
"dependencies": {
14+
"@azure/msal-browser": "^3.10.0",
15+
"@types/node": "^18.19.26",
16+
"express": "^4.18.3",
17+
"open": "^10.1.0",
18+
"typescript": "^5.4.2",
19+
"webpack": "^5.90.3"
20+
},
21+
"devDependencies": {
22+
"webpack-cli": "^5.1.4"
23+
}
24+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# AppRoot Upload Session
2+
3+
This sample shows how to create a basic upload session for large files using an application's [special approot folder](https://learn.microsoft.com/graph/api/drive-get-specialfolder).
4+
5+
## Setup
6+
7+
### Create a new Application Registration in Entra ID
8+
9+
1. Creat a new application registration
10+
2. Add a SPA authentication method
11+
- Ensure you allow Access Tokens and ID Tokens
12+
- Redirect: https://localhost:8080
13+
3. Consent to the `Files.ReadWrite` or `Files.ReadWrite.AppFolder` (least privledge) delegated scope
14+
4. Create a file
15+
16+
### Update application info
17+
18+
1. In [security.ts](./src/security.ts) update the `authority` and `clientId` to match those of the application registration
19+
2. In [security.ts](./src/security.ts) ensure that the scopes listed in the `loginRequest` object match the scopes consented for your application
20+
21+
## Run
22+
23+
1. Ensure all dependencies are installed `npm install`
24+
2. Start the application `npm start`
25+
3. A browser window should auto-launch, if not open a browser and load `http://localhost:8080/`
26+
4. Using the UX select a file and then select "Do Upload"
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { applyToken } from "./security.js";
2+
3+
const btn = document.getElementById("go");
4+
const log = <HTMLDivElement>document.getElementById("log");
5+
6+
function logger(message: string) {
7+
8+
const p = document.createElement("p");
9+
p.innerHTML = message;
10+
log.prepend(p);
11+
}
12+
13+
btn.addEventListener("click", async (e) => {
14+
15+
e.preventDefault();
16+
17+
const input = <HTMLInputElement>document.getElementById("the-file");
18+
19+
20+
if (input.files && input.files.length > 0) {
21+
22+
const file = input.files[0];
23+
24+
const getFolderInit = await applyToken({
25+
method: "GET",
26+
});
27+
28+
const folderInfoResponse = await fetch("https://graph.microsoft.com/v1.0/me/drive/special/approot", getFolderInit);
29+
30+
const folderInfo = await folderInfoResponse.json();
31+
32+
const { driveId } = folderInfo.parentReference;
33+
34+
// rebase the URL to the /drives/{id} pattern
35+
const baseUrl = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${folderInfo.id}`;
36+
37+
// 1. create upload session
38+
const createUploadSessionUrl = `${baseUrl}:/${encodeURIComponent(file.name)}:/createUploadSession`;
39+
const createUploadSessionInit = await applyToken({
40+
method: "POST",
41+
body: JSON.stringify({
42+
item: {
43+
"@microsoft.graph.conflictBehavior": "rename",
44+
name: file.name,
45+
},
46+
}),
47+
headers: {
48+
"Content-Type": "application/json",
49+
}
50+
});
51+
const uploadSessionInfoResponse = await fetch(createUploadSessionUrl, createUploadSessionInit);
52+
const uploadSessionInfo = await uploadSessionInfoResponse.json();
53+
if (!uploadSessionInfoResponse.ok) {
54+
throw Error(JSON.stringify(uploadSessionInfo, null, 2));
55+
}
56+
logger(`Created upload session: <pre>${JSON.stringify(uploadSessionInfo, null, 2)}</pre>`);
57+
58+
// this is the upload url for our new file, it is opaque
59+
const { uploadUrl } = uploadSessionInfo;
60+
61+
// 2. Get a stream representing the file
62+
const stream = file.stream();
63+
const reader = stream.getReader();
64+
65+
// 3. Use the reader pattern (or any other) to upload the file
66+
let bytePointer = 0;
67+
let uploadBytesInfo;
68+
reader.read().then(async function pump({ done, value }) {
69+
70+
if (done) {
71+
72+
if (value) {
73+
console.error("had value at done");
74+
}
75+
76+
logger(`Upload complete <a href="${uploadBytesInfo.webUrl}" target="_blank">${uploadBytesInfo.name}</a>`);
77+
78+
return;
79+
}
80+
81+
const contentLength = value.length - 1;
82+
const contentRangeStr = `bytes ${bytePointer}-${bytePointer + contentLength}/${(file.size)}`;
83+
logger(`Uploading: ${contentRangeStr}`);
84+
85+
// 4. Execute a series of PUT commands to upload the file in chunks
86+
const uploadBytesInit = await applyToken({
87+
method: "PUT",
88+
body: value,
89+
headers: {
90+
"Content-Length": contentLength.toString(),
91+
"Content-Range": contentRangeStr,
92+
}
93+
});
94+
const uploadBytesInfoResponse = await fetch(uploadUrl, uploadBytesInit);
95+
uploadBytesInfo = await uploadBytesInfoResponse.json();
96+
97+
if (!uploadBytesInfoResponse.ok) {
98+
throw Error(JSON.stringify(uploadBytesInfo));
99+
}
100+
101+
bytePointer += value.length;
102+
103+
// Read some more, and call this function again
104+
return reader.read().then(pump);
105+
});
106+
107+
} else {
108+
109+
alert("Please select a file.");
110+
}
111+
});
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { PublicClientApplication } from "@azure/msal-browser";
2+
3+
const client = new PublicClientApplication({
4+
auth: {
5+
authority: "https://login.microsoftonline.com/{tenant id}/",
6+
clientId: "{app id}",
7+
redirectUri: "http://localhost:8080",
8+
}
9+
});
10+
11+
await client.initialize();
12+
13+
export async function applyToken(init: Partial<RequestInit>): Promise<Partial<RequestInit>> {
14+
15+
const loginRequest = {
16+
scopes: ["Files.ReadWrite.AppFolder", "User.Read"],
17+
};
18+
19+
let token = "";
20+
21+
try {
22+
23+
let tokenResponse = await client.acquireTokenSilent(loginRequest);
24+
token = tokenResponse.accessToken;
25+
26+
} catch (e) {
27+
28+
const resp = await client.loginPopup(loginRequest);
29+
30+
if (resp.idToken) {
31+
32+
client.setActiveAccount(resp.account);
33+
34+
const resp2 = await client.acquireTokenSilent(loginRequest);
35+
token = resp2.accessToken;
36+
37+
} else {
38+
// throw the error that brought us here
39+
throw e;
40+
}
41+
}
42+
43+
if (typeof init.headers === "undefined") {
44+
init.headers = {};
45+
}
46+
47+
init.headers = {
48+
"Authorization": `Bearer ${token}`,
49+
...init.headers,
50+
}
51+
52+
return init;
53+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import express from "express";
2+
import { dirname, resolve } from "path";
3+
import { fileURLToPath } from "url";
4+
import open from "open";
5+
6+
7+
const __dirname = dirname(fileURLToPath(import.meta.url));
8+
9+
const server = express();
10+
const port = 8080;
11+
12+
// setup static paths
13+
server.use("/bundles", express.static(resolve(__dirname, "./bundles")));
14+
server.use(express.static(resolve(__dirname, "../static")));
15+
16+
// start the server
17+
server.listen(port, () => {
18+
19+
// open the default browser to the sample page
20+
open("http://localhost:8080");
21+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import webpack from "webpack";
2+
import { resolve, dirname } from "path";
3+
import { fileURLToPath } from "url";
4+
5+
const __dirname = dirname(fileURLToPath(import.meta.url));
6+
7+
const NODE_ENV = "development";
8+
9+
export default <webpack.Configuration[]>[
10+
{
11+
devtool: "eval-source-map",
12+
entry: resolve(__dirname, "../build/main.js"),
13+
mode: NODE_ENV,
14+
output: {
15+
library: {
16+
name: 'BrowserEntry',
17+
type: 'umd',
18+
},
19+
filename: "client-bundle.js",
20+
path: resolve(__dirname, "../build/bundles"),
21+
},
22+
stats: {
23+
assets: false,
24+
colors: true,
25+
},
26+
}
27+
];
16.8 KB
Binary file not shown.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<html>
2+
3+
<head>
4+
<title>Testing</title>
5+
</head>
6+
7+
<body>
8+
9+
<input type="file" id="the-file">Select a file to upload</input>
10+
<br />
11+
<br />
12+
<button id="go">Do Upload</button>
13+
<br />
14+
<br />
15+
16+
<div id="log" style="font-size: x-small;max-height: 600px;"></div>
17+
18+
<script type="text/javascript" src="/bundles/client-bundle.js"></script>
19+
</body>
20+
21+
</html>

0 commit comments

Comments
 (0)