Skip to content

LargeFileUpload - node js stream support and progress handling, rollup library updates #401

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Mar 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 37 additions & 33 deletions design/publishing.md
Original file line number Diff line number Diff line change
@@ -1,73 +1,77 @@
##### This design document focuses on the following -
##### This design document focuses on the following -

1. Separate entry points for node and browser.
2. Specifying the browser field in package.json.
3. Changes in the bundling process.

##### Terms -

* bundler - Module bundlers are tools frontend developers used to bundle JavaScript modules into a single JavaScript files that can be executed in the browser.
- bundler - Module bundlers are tools frontend developers used to bundle JavaScript modules into a single JavaScript files that can be executed in the browser.

- rollup - rollup.js is the module bundler that the JS SDK uses.

* rollup - rollup.js is the module bundler that the JS SDK uses.
- package.json fields -

* package.json fields -
* main - The main field is a module ID that is the primary entry point to your program. Points to the CJS modules.
- main - The main field is a module ID that is the primary entry point to your program. Points to the CJS modules.

* module - The module field is not an official npm feature but a common convention among bundlers to designate how to import an ESM version of a library. Points to the ES modules.
- module - The module field is not an official npm feature but a common convention among bundlers to designate how to import an ESM version of a library. Points to the ES modules.

* browser - If the module is meant to be used client-side, the browser field should be used instead of the main field.
- browser - If the module is meant to be used client-side, the browser field should be used instead of the main field.

##### Current set up -

1.
TypeScript Source Code
1. TypeScript Source Code
/ \
Transpiles into JavaScript
Transpiles into JavaScript
'lib' folder
/ \
CJS module ES modules
2. main - `lib/src/index.js`
module - `lib/es/src/index.js`
2. main - `lib/src/index.js` module - `lib/es/src/index.js`

3. Rollup bundling output

- `graph-js-sdk.js` - Bundled and minified file in IIFE format. This file can be directly used in the browser with a `<script>` tag.
- `graph-es-sdk.js` - Bundled file in ES format.

3. Rollup bundling output
* `graph-js-sdk.js` - Bundled and minified file in IIFE format. This file can be directly used in the browser with a `<script>` tag.
* `graph-es-sdk.js` - Bundled file in ES format.
4. Entry point for rollup - `lib/es/src/browser/index.js`.

##### Difference between src/index.js and src/browser/index.js
1. `src/browser/index.js` does not export `RedirectHandler` and `RedirectHandlerOptions`. Redirection is handled by the browser.

1. `src/browser/index.js` does not export `RedirectHandler` and `RedirectHandlerOptions`. Redirection is handled by the browser.
2. `src/browser/index.js` exports `src/browser/ImplicitMsalProvider`.
3. `src/browser/ImplicitMsalProvider` does not import or require 'msal' dependency. While,
`src/ImplicitMsalProvider` imports or requires 'msal' in the implementation.
3. `src/browser/ImplicitMsalProvider` does not import or require 'msal' dependency. While, `src/ImplicitMsalProvider` imports or requires 'msal' in the implementation.
4. My assumtion is that `src/browser/ImplicitMsalProvider` is implemented specifically for the rollup process and to skip the rollup external dependency while using `graph-js-sdk.js` in the browser.

Note - Browser applications using the ES modules from the npm package of the JS SDK refer to the `module` entry point - `lib/es/src/index.js`(not the browser entry point). Example - Graph Explorer.

##### Upcoming changes -
##### Upcoming changes -

1. Use the browser field for the following -

* The Graph JS SDK currently has two entry files, `src/index` and `src/browser/index`.
Use the browser field to indicate the browser entry point.
Example -
- The Graph JS SDK currently has two entry files, `src/index` and `src/browser/index`. Use the browser field to indicate the browser entry point. Example -

```json
"browser":
{ "lib/es/src/index.js": "lib/es/src/browser/index.js" }
```

Currently, the main and "module field in the package.json. This will remain the same.
* Better way to handle environment specific implementation. For example, using the `browser` field as follows -
"browser":

- Better way to handle environment specific implementation. For example, using the `browser` field as follows - "browser":

```json
{
"stream": "stream-browserify",
"Feature-Node.js": "Feature-Browser.js"
}
{
"stream": "stream-browserify",
"Feature-Node.js": "Feature-Browser.js"
}
```

2. Remove export of `src/browser/ImplicitMsalProvider` from `src/browser/index`.
* Till `ImplicitMsalProvider` is maintained in the repo, maintain a separate entry point say `rollup-index` for the rollup process which exports `src/browser/index` and `src/browser/ImplicitMsalProvider`.
* Continue rolling up the `src/browser/ImplicitMsalProvider` as it is currently done and not introduce breaking changes here as it is going to be deprecated.
* Remove the separate entry point once `ImplicitMsalProvider` is removed and use the browser entry point for roll up thereafter. The goal is to maintain a consistent entry point or usage for browser applications using the JS SDK and the rollup/bundling process.
2. Remove export of `src/browser/ImplicitMsalProvider` from `src/browser/index`.

- Till `ImplicitMsalProvider` is maintained in the repo, maintain a separate entry point say `rollup-index` for the rollup process which exports `src/browser/index` and `src/browser/ImplicitMsalProvider`.
- Continue rolling up the `src/browser/ImplicitMsalProvider` as it is currently done and not introduce breaking changes here as it is going to be deprecated.
- Remove the separate entry point once `ImplicitMsalProvider` is removed and use the browser entry point for roll up thereafter. The goal is to maintain a consistent entry point or usage for browser applications using the JS SDK and the rollup/bundling process.

3. Bundle the authproviders separately as they are optional.

4. Stop bundling in ES format, that is remove `graph-es-sdk.js` as the ES modules are being shipped.
4. Stop bundling in ES format, that is remove `graph-es-sdk.js` as the ES modules are being shipped.
9 changes: 2 additions & 7 deletions docs/TokenCredentialAuthenticationProvider.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,6 @@ The browser use of the file is as follows:

```html
<!-- include the script -->
<script type="text/javascript" src="<PATH_TO_SCRIPT>/graph-client-tokenCredentialAuthProvider.js"></script>;
// create an authProvider
var authProvider = new MicrosoftGraph.TokenCredentialAuthProvider.TokenCredentialAuthenticationProvider(tokenCred, { scopes: scopes });

client = MicrosoftGraph.Client.initWithMiddleware({
authProvider: authProvider,
});
<script type="text/javascript" src="<PATH_TO_SCRIPT>/graph-client-tokenCredentialAuthProvider.js"></script>
; // create an authProvider var authProvider = new MicrosoftGraph.TokenCredentialAuthProvider.TokenCredentialAuthenticationProvider(tokenCred, { scopes: scopes }); client = MicrosoftGraph.Client.initWithMiddleware({ authProvider: authProvider, });
```
226 changes: 134 additions & 92 deletions docs/tasks/LargeFileUploadTask.md
Original file line number Diff line number Diff line change
@@ -1,85 +1,159 @@
# Large File Upload Task - Uploading large files to OneDrive
# Large File Upload Task - Uploading large files to OneDrive, Outlook, Print API.

This task simplifies the implementation of OneDrive's [resumable upload](https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/driveitem_createuploadsession).
References -

- [Outlook's large file attachment](https://docs.microsoft.com/en-us/graph/outlook-large-attachments)
- [OneDrive's resumable upload](https://docs.microsoft.com/en-us/graph/api/driveitem-createuploadsession?view=graph-rest-1.0&preserve-view=true)
- [Print API's large file upload](https://docs.microsoft.com/en-us/graph/upload-data-to-upload-session)

## Creating the client instance

Refer [this documentation](../CreatingClientInstance.md) for initializing the client.

## Uploading from browser
## Using the LargeFileUpload Task

#### Create an upload session

First step for any upload task is the creation of the upload session.

HTML to select the file for uploading.
**Example of a payload for Outlook**

```HTML
<input id="fileUpload" type="file" onchange="fileUpload(this)" />
```typescript
const payload = {
AttachmentItem: {
attachmentType: "file",
name: "<FILE_NAME>",
size: FILE_SIZE,
},
};
```

Get files from the input element and start uploading.
**Example of a payload for OneDrive**

```typescript
async function fileUpload(elem) {
let file = elem.files[0];
try {
let response = await largeFileUpload(client, file, file.name);
console.log(response);
console.log("File Uploaded Successfully.!!");
} catch (error) {
console.error(error);
}
}
const payload = {
item: {
"@microsoft.graph.conflictBehavior": "rename",
name: "<FILE_NAME>",
},
};
```

async function largeFileUpload(client, file) {
try {
let options = {
path: "/Documents",
fileName: file.name,
rangeSize: 1024 * 1024,
};
const uploadTask = await MicrosoftGraph.OneDriveLargeFileUploadTask.create(client, file, options);
const response = await uploadTask.upload();
return response;
} catch (err) {
throw err;
}
}
**Create the upload session**

```typescript
const uploadSession = LargeFileUploadTask.createUploadSession(client, "REQUEST_URL", payload);
```

## Uploading from NodeJS
#### Creating the LargeFileUploadTask object

- To create the LargeFileUploadTask object you need to create -
- An upload session as shown above.
- A `FileObject` instance.

**FileObject Interface**

```typescript
function uploadFile() {
fs.readFile("<PATH_OF_THE_FILE>", {}, function(err, file) {
if (err) {
throw err;
}
let fileName = "<NAME_OF_THE_FILE_WITH_EXTN>";
oneDriveLargeFileUpload(client, file, fileName)
.then((response) => {
console.log(response);
console.log("File Uploaded Successfully.!!");
})
.catch((error) => {
throw err;
});
});
export interface FileObject<T> {
content: T;
name: string;
size: number;
sliceFile(range: Range): Promise<ArrayBuffer | Blob | Buffer>;
}
```

The Microsoft Graph JavaScript Client SDK provides two implementions -

1. StreamUpload - Supports Node.js stream upload

```typescript
import StreamUpload from "@microsoft/microsoft-graph-client";
import * as fs from "fs";

const fileName = "<FILE_NAME>";
const stats = fs.statSync(`./test/sample_files/${fileName}`);
const totalsize = stats.size;
const readStream = fs.createReadStream(`./test/sample_files/${fileName}`);
const fileObject = new StreamUpload(readStream, fileName, totalsize);
```

Note - In case of a browser application, you can use [stream-browserify](https://www.npmjs.com/package/stream-browserify) and [buffer](https://www.npmjs.com/package/buffer).

2. FileUpload - Supports upload of file formats - ArrayBuffer, Blob, Buffer

```typescript
import FileUpload from "@microsoft/microsoft-graph-client";
import * as fs from "fs";

const fileName = "<FILE_NAME>";
const stats = fs.statSync(`./test/sample_files/${fileName}`);
const totalsize = stats.size;
const readStream = fs.readFileSync(`./test/sample_files/${fileName}`);
const fileObject = new FileUpload(readStream, fileName, totalsize);
```

async function oneDriveLargeFileUpload(client, file, fileName) {
try {
let options = {
path: "/Documents",
fileName,
rangeSize: 1024 * 1024,
};
const uploadTask = await OneDriveLargeFileUploadTask.create(client, file, options);
const response = await uploadTask.upload();
return response;
} catch (err) {
console.log(err);
}
**_Note_** - You can also have a customized `FileObject` implementation which contains the `sliceFile(range: Range)` function which implements the logic to split the file into ranges.

**Initiate the LargefileUploadTask options with Progress Handler and Range Size**

```typescript
const progress = (range?: Range, extraCallBackParams?: unknown) => {
// Handle progress event
};

const progressCallBack: Progress = {
progress,
extraCallBackParam, // additional parameters to the callback
};

const options: LargeFileUploadTaskOptions = {
rangeSize: 327680,
progressCallBack: progressCallBack,
};
```

**Create a LargefileUploadTask object**

```typescript
const uploadTask = new LargeFileUploadTask(client, fileObj, uploadSession, optionsWithProgress);
const uploadResult: UploadResult = await uploadTask.upload();
```

`UploadResult` contains the `location`(received in the Outlook API response headers) and the `responseBody` (responseBody received after successful upload.) properties.

## OneDriveLargeFileUploadTask.

_You can also use `OneDriveLargeFileUploadTask` which provides easier access to upload to OneDrive API_

Example -

```typescript
const progressCallBack: Progress = {
progress,
completed,
failure,
extraCallBackParams: true,
};

const options: OneDriveLargeFileUploadOptions = {
path: "/Documents",
fileName,
rangeSize: 1024 * 1024,
progressCallBack,
};
const readStream = fs.createReadStream(`./fileName`);
const fileObject = new StreamUpload(readStream, fileName, totalsize);
or
const readFile = fs.readFileSync(`./fileName`);
const fileObject = new FileUpload(readStream, fileName, totalsize);

const uploadTask = await OneDriveLargeFileUploadTask.createTaskWithFileObject(client, fileObject, options);
const uploadResult:UploadResult = await uploadTask.upload();
}
```

> Note: The `OneDriveLargeFileUploadTask.createTaskWithFileObject` also handles the upload session creation.**

## We can just resume the broken upload

_Lets consider some break down happens in the middle of uploading, with the uploadTask object in hand you can resume easily._
Expand All @@ -98,38 +172,6 @@ let slicedFile = uploadTask.sliceFile(range);
uploadTask.uploadSlice(slicedFile, range, uploadTask.file.size);
```

## Uploading with custom options

_You can pass in the customized options using LargeFileUploadTask_

```typescript
async function largeFileUpload(client, file) {
const fileName = file.name;
const driveId = "<YOUR_DRIVE_ID>";
const path = "<LOCATION_TO_STORE_FILE>";
try {
const requestUrl = `/drives/${driveId}/root:${path}/${fileName}:/createUploadSession`;
const payload = {
item: {
"@microsoft.graph.conflictBehavior": "fail",
name: fileName,
},
};
const fileObject = {
size: file.size,
content: file,
name: fileName,
};
const uploadSession = await LargeFileUploadTask.createUploadSession(client, requestUrl, payload);
const uploadTask = await new LargeFileUploadTask(client, fileObject, uploadSession);
const response = await uploadTask.upload();
return response;
} catch (err) {
throw err;
}
}
```

## Cancelling a largeFileUpload task

_Cancelling an upload session sends a DELETE request to the upload session URL_
Expand Down
Loading