|
#!/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*) |