Skip to content

Commit

Permalink
Initial code commit - testing action
Browse files Browse the repository at this point in the history
  • Loading branch information
mikedeboer committed Dec 5, 2024
1 parent dd19324 commit a585e52
Show file tree
Hide file tree
Showing 8,346 changed files with 1,245,230 additions and 2 deletions.
The diff you're trying to view is too large. We only load the first 3000 changed files.
11 changes: 11 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
root = true

[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ bower_components
build/Release

# Dependency directories
node_modules/
# node_modules/
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)
Expand Down Expand Up @@ -89,7 +89,7 @@ out

# Nuxt.js build / generate output
.nuxt
dist
# dist

# Gatsby files
.cache/
Expand Down
7 changes: 7 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"endOfLine": "auto",
"tabWidth": 2,
"printWidth": 100,
"semi": true,
"singleQuote": false
}
25 changes: 25 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: "sharepoint-download-action"
description: "GitHub Action for downloading a single item from Microsoft SharePoint"
author: "MedicalVR BV"
branding:
icon: "download"
color: "green"
inputs:
azure-client-id:
description: "Azure Client ID"
required: true
azure-client-secret:
description: "Azure Client Secret"
required: true
azure-tenant-id:
description: "Azure Tenant ID"
required: true
uri:
description: "Target location of the file as URI, e.g. 'sharepoint://Team/Builds/Automation/alpha/v124/file.zip'"
required: true
target:
description: "Target absolute path to download the file to"
required: true
runs:
using: "node20"
main: "dist/index.js"
31 changes: 31 additions & 0 deletions dist/client.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import "isomorphic-fetch";
import { Client } from "@microsoft/microsoft-graph-client";
export type DriveItem = {
id: string;
path: string;
};
export type Group = {
id: string;
name: string;
};
export type GroupDrive = {
id: string;
groupId: string;
};
export type SharePointClientConfig = {
tenantId: string;
clientId: string;
clientSecret: string;
};
export declare class SharePointClient {
private readonly config;
private clientSecretCredential;
private graph;
constructor(config: SharePointClientConfig);
getGraph(): Client;
getGroup(name: string): Promise<Group | undefined>;
getDriveItem(group: Group, path: string): Promise<DriveItem | undefined>;
downloadItem(group: Group, path: string, targetPath: string): Promise<void>;
uploadItem(group: Group, path: string, targetPath: string, callback: (progress: number) => void): Promise<DriveItem>;
}
export declare const getClient: (config: SharePointClientConfig) => SharePointClient;
139 changes: 139 additions & 0 deletions dist/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getClient = exports.SharePointClient = void 0;
const tslib_1 = require("tslib");
require("isomorphic-fetch");
const identity_1 = require("@azure/identity");
const microsoft_graph_client_1 = require("@microsoft/microsoft-graph-client");
const node_fs_1 = require("node:fs");
const Path = tslib_1.__importStar(require("node:path"));
const node_util_1 = require("node:util");
const promises_1 = require("node:fs/promises");
const Stream = tslib_1.__importStar(require("node:stream"));
const azureTokenCredentials_1 = require("@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials");
const Finished = (0, node_util_1.promisify)(Stream.finished);
const TEMP_FILE_CONTENTS = "Temporary contents...";
class SharePointClient {
constructor(config) {
this.config = config;
this.clientSecretCredential = new identity_1.ClientSecretCredential(this.config.tenantId, this.config.clientId, this.config.clientSecret);
}
getGraph() {
if (this.graph) {
return this.graph;
}
const authProvider = new azureTokenCredentials_1.TokenCredentialAuthenticationProvider(this.clientSecretCredential, {
scopes: ["https://graph.microsoft.com/.default"],
});
this.graph = microsoft_graph_client_1.Client.initWithMiddleware({ authProvider });
return this.graph;
}
async getGroup(name) {
const groups = await this.getGraph().api("/groups").select(["displayName", "id"]).get();
const nameLC = name.toLowerCase();
for (const group of groups.value) {
if (group.displayName.toLowerCase() === nameLC) {
return {
id: group.id,
name: group.displayName,
};
}
}
return undefined;
}
async getDriveItem(group, path) {
const res = await this.getGraph()
.api(`/groups/${group.id}/drive/root:/${path}`)
.select(["id"])
.get();
if (res) {
return {
id: res.id,
path,
};
}
return undefined;
}
async downloadItem(group, path, targetPath) {
const stream = await this.getGraph()
.api(`/groups/${group.id}/drive/root:/${path}:/content`)
.responseType(microsoft_graph_client_1.ResponseType.BLOB)
.get();
if (stream) {
const writer = (0, node_fs_1.createWriteStream)(targetPath);
Stream.Readable.fromWeb(stream instanceof Blob ? stream.stream() : stream).pipe(writer);
await Finished(writer);
}
}
async uploadItem(group, path, targetPath, callback) {
const pathDetails = Path.parse(targetPath);
let item;
try {
item = await this.getDriveItem(group, targetPath);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
}
catch (ex) {
// Do nothing.
}
const graph = this.getGraph();
if (!item) {
// First we need to create a new (placeholder) item that's located at the same path.
const parent = await this.getDriveItem(group, pathDetails.dir);
if (!parent) {
throw new Error(`Directory not found on SharePoint of group ${group.name}: ${pathDetails.dir}`);
}
const res = await graph
.api(`/groups/${group.id}/drive/items/${parent.id}:/${pathDetails.base}:/content`)
.put(TEMP_FILE_CONTENTS);
item = {
id: res.id,
path: targetPath,
};
}
if (!item) {
throw new Error(`Drive item at path ${targetPath} could not be created.`);
}
const stats = await (0, promises_1.stat)(path);
const totalSize = stats.size;
const progress = (range) => {
if (!range) {
callback(100);
return;
}
const current = range.maxValue;
const newer = Math.min(totalSize, current);
const old = Math.max(totalSize, current);
const frac = Math.abs(newer - old) / old;
callback(100 - Math.floor(frac * 100));
};
const uploadEventHandlers = {
progress,
};
const options = {
rangeSize: 1024 * 1024,
uploadEventHandlers,
};
// Create upload session for SharePoint upload.
const payload = {
item: {
"@microsoft.graph.conflictBehavior": "replace",
},
};
const reader = (0, node_fs_1.createReadStream)(path);
const uploadSession = await microsoft_graph_client_1.LargeFileUploadTask.createUploadSession(graph, `https://graph.microsoft.com/v1.0/groups/${group.id}/drive/items/${item.id}/createuploadsession`, payload);
const fileObject = new microsoft_graph_client_1.StreamUpload(reader, pathDetails.base, totalSize);
const task = new microsoft_graph_client_1.LargeFileUploadTask(graph, fileObject, uploadSession, options);
await task.upload();
return item;
}
}
exports.SharePointClient = SharePointClient;
let gClient = undefined;
const getClient = (config) => {
if (gClient) {
return gClient;
}
gClient = new SharePointClient(config);
return gClient;
};
exports.getClient = getClient;
10 changes: 10 additions & 0 deletions dist/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { BigIntStats, Stats } from "node:fs";
import { SharePointClientConfig } from "./client";
export type ExtStats = (Stats | BigIntStats) & {
exists: boolean;
};
export type StatOptions = {
bigint: boolean;
};
export declare const statExt: (path: string, options?: StatOptions) => Promise<ExtStats>;
export declare const downloadFile: (uri: string, to: string, config: SharePointClientConfig) => Promise<void>;
81 changes: 81 additions & 0 deletions dist/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.downloadFile = exports.statExt = void 0;
const tslib_1 = require("tslib");
const client_1 = require("./client");
const core_1 = require("@actions/core");
const promises_1 = require("node:fs/promises");
const Path = tslib_1.__importStar(require("node:path"));
const groupsMap = new Map();
const decomposeSharePointURI = async (uri, client) => {
const [groupName, ...pathParts] = uri.replace("sharepoint://", "").split("/");
// sharepoint://Team/Builds/Automation/alpha/v2.1.4/docs/library.pdf
// [___________][___][______________________________________________]
// | | |
// Pseudo- Group Path to file on Team's Shared Documents
// protocol name (relative to root)
if (!groupName) {
throw new Error(`No group name could be found in URI '${uri}'`);
}
const groupNameLC = groupName.toLowerCase();
if (!groupsMap.has(groupNameLC)) {
const res = await client.getGroup(groupNameLC);
if (res) {
groupsMap.set(groupNameLC, res);
}
}
const group = groupsMap.get(groupNameLC);
if (!group) {
throw new Error(`Could not find SharePoint group ${groupName}`);
}
return { group, pathParts };
};
const statExt = async (path, options) => {
let s;
try {
s = await (0, promises_1.stat)(path, options);
return Object.assign(s, { exists: true });
// eslint-disable-next-line @typescript-eslint/no-unused-vars
}
catch (ex) {
// no-op.
}
return { ...{}, exists: false };
};
exports.statExt = statExt;
const downloadFile = async (uri, to, config) => {
if (!uri.startsWith("sharepoint:")) {
throw new Error(`Invalid URI: ${uri}`);
}
const client = (0, client_1.getClient)(config);
const { group, pathParts } = await decomposeSharePointURI(uri, client);
const fileName = pathParts[pathParts.length - 1];
if (!fileName) {
throw new Error(`Invalid URI: ${uri}`);
}
(0, core_1.info)(`Downloading '${uri}' to '${to}'...`);
const itemPath = pathParts.join("/");
// Try once to create the directory if it doesn't exist yet. No mkdirp.
const stat = await (0, exports.statExt)(to);
if (!stat.exists) {
await (0, promises_1.mkdir)(to);
}
try {
// This fetch may yield a 404 error and is cheaper than a full download.
await client.getDriveItem(group, itemPath);
await client.downloadItem(group, itemPath, Path.join(to, fileName));
}
catch (ex) {
throw new Error(`Could not download '${itemPath}' with error ${ex.message}`);
}
(0, core_1.info)("done!");
};
exports.downloadFile = downloadFile;
(async function () {
const config = {
clientId: (0, core_1.getInput)("azure-client-id"),
clientSecret: (0, core_1.getInput)("azure-client-secret"),
tenantId: (0, core_1.getInput)("azure-tenant-id"),
};
await (0, exports.downloadFile)((0, core_1.getInput)("uri"), (0, core_1.getInput)("target"), config);
})();
43 changes: 43 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import typescriptEslint from "@typescript-eslint/eslint-plugin";
import globals from "globals";
import tsParser from "@typescript-eslint/parser";
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
});

export default [
...compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"),
{
plugins: {
"@typescript-eslint": typescriptEslint,
},

languageOptions: {
globals: {
...globals.browser,
...globals.jest,
},

parser: tsParser,
ecmaVersion: 12,
sourceType: "module",
},

rules: {
"no-unused-vars": "off",
indent: "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-object-type": "off",
},
},
];
15 changes: 15 additions & 0 deletions node_modules/.bin/acorn

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions node_modules/.bin/acorn.cmd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit a585e52

Please sign in to comment.