Skip to content

Instantly share code, notes, and snippets.

@jacobemcken
Created April 7, 2024 15:51
Show Gist options
  • Save jacobemcken/6846e4c92b27960529a7794367784cd8 to your computer and use it in GitHub Desktop.
Save jacobemcken/6846e4c92b27960529a7794367784cd8 to your computer and use it in GitHub Desktop.
Upload file to SharePoint with app-only principal using Babashka
#!/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