Instantly share code, notes, and snippets.
Created
April 7, 2024 15:51
-
Star
(0)
0
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
Save jacobemcken/6846e4c92b27960529a7794367784cd8 to your computer and use it in GitHub Desktop.
Upload file to SharePoint with app-only principal using Babashka
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bb | |
(ns upload-to-sharepoint | |
(:require [babashka.http-client :as http] | |
[cheshire.core :as json] | |
[clojure.java.io :as io] | |
[clojure.string :as str] | |
[clojure.tools.cli :as cli]) | |
(:import (java.net URLEncoder URLConnection))) | |
(def cli-options | |
[[nil "--client-id ID" "Name of the environment variable holding the SharePoint client id"] | |
[nil "--client-secret SECRET" "Name of the environment variable holding the SharePoint client secret"] | |
[nil "--mime-type MIME-TYPE" "Force mime type to overwrite the default guess"]]) | |
(defn percent-encode | |
[s] | |
(-> s | |
(URLEncoder/encode) | |
(str/replace "+" "%20"))) | |
(def sharepoint-principal-id | |
"00000003-0000-0ff1-ce00-000000000000") | |
(def tenant-name | |
"contoso") | |
(def tenant-id | |
"contoso.com") | |
(def endpoint | |
(str "https://" tenant-name ".sharepoint.com/sites/" "somesite")) | |
;; Put me on the right track | |
;; https://www.c-sharpcorner.com/article/how-to-perform-sharepoint-app-only-authentication-in-power-automate/ | |
(defn get-token | |
[{:keys [client-id client-secret tenant-id tenant-name]}] | |
(let [params {"client_id" (str client-id "@" tenant-id ) | |
"client_secret" client-secret | |
"grant_type" "client_credentials" | |
"resource" (str sharepoint-principal-id "/" tenant-name ".sharepoint.com@" tenant-id) }] | |
(-> (http/post (str "https://accounts.accesscontrol.windows.net/" tenant-id "/tokens/OAuth/2") | |
{:form-params params}) | |
:body | |
(json/decode true)))) | |
(defn upload-file | |
[access-token endpoint {:keys [input-stream name destination] :as _file-info} opts] | |
(let [mime-type (or (:mime-type opts) | |
(not-empty (URLConnection/guessContentTypeFromName name)))] | |
(when-not mime-type | |
(throw (ex-info "Unable to automatically determine mime-type" {:file-name name}))) | |
(println "Uploading" (str destination "/" name)) | |
(http/post (str endpoint "/_api/web" | |
"/GetFolderByServerRelativeUrl('" (percent-encode destination) "')" | |
"/Files/add(url='" (percent-encode name) "',overwrite=true)") | |
{:headers {"Authorization" (str "Bearer " access-token) | |
"Content-Type" mime-type} | |
:body input-stream}))) | |
(defn create-folder | |
[access-token endpoint folder-name] | |
(http/post (str endpoint "/_api/web/Folders/add('" (percent-encode folder-name) "')" ) | |
{:version :http1.1 ; SharePoint API gets confused about empty body when using HTTP v. 2 | |
:headers {"Authorization" (str "Bearer " access-token)}})) | |
(defn single-upload-info | |
[file destination] | |
{:destination destination | |
:input-stream file | |
:name (.getName file)}) | |
(defn upload-info-keep-relative-path | |
[local-base-path destination file] | |
(let [file-name (.getName file) | |
relative-path (subs (.getCanonicalPath file) | |
(count local-base-path) | |
(- (count (.getCanonicalPath file)) (if (.isFile file) | |
(+ (count file-name) 1) | |
0)))] | |
{:destination (str destination relative-path) | |
:input-stream file | |
:name file-name})) | |
(defn upload-dir | |
[access-token source-dir destination opts] | |
(->> (file-seq source-dir) | |
(mapv (partial upload-info-keep-relative-path (.getCanonicalPath source-dir) destination)) | |
(mapv #(if (.isFile (:input-stream %)) | |
(upload-file access-token endpoint % opts) | |
(create-folder access-token endpoint (:destination %)))))) | |
(defn main | |
[& args] | |
(let [{:keys [options arguments _summary]} (cli/parse-opts args cli-options) | |
[source destination] arguments | |
token-resp (get-token {:client-id (:client-id options) | |
:client-secret (:client-secret options) | |
:tenant-id tenant-id | |
:tenant-name tenant-name}) | |
access-token (:access_token token-resp) | |
file-or-dir (io/file source)] | |
(when-not (.exists file-or-dir) | |
(throw (ex-info "Source path doesn't exist" {:source source}))) | |
(if (.isFile file-or-dir) | |
(upload-file access-token endpoint (single-upload-info file-or-dir destination) options) | |
(upload-dir access-token file-or-dir destination options)))) | |
(apply main *command-line-args*) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment