Class: Service::SharepointService
- Inherits:
-
Service
- Object
- Service
- Service::SharepointService
- Defined in:
- lib/active_storage/service/sharepoint_service.rb
Overview
SharePoint Storage Service
Implements the Active Storage service interface to interact with Microsoft 365 SharePoint via the Microsoft Graph API.
Overview
This service allows Rails Active Storage to use Microsoft 365 SharePoint as a file storage backend. It handles all file operations (upload, download, delete, check existence) by communicating with SharePoint via the Microsoft Graph API.
Configuration
Configure in your storage.yml:
# config/storage.yml
sharepoint:
service: Sharepoint
ms_graph_url: https://graph.microsoft.com
ms_graph_version: v1.0
auth_host: https://login.microsoftonline.com
tenant_id: your-tenant-id
app_id: your-app-id
secret: your-client-secret
site_id: your-site-id
drive_id: your-drive-id
Then activate in your environment config:
config.active_storage.service = :sharepoint
Usage
Use normally with Active Storage in your models:
class Document < ApplicationRecord
has_one_attached :file
end
doc = Document.new
doc.file.attach(io: File.open("document.pdf"), filename: "doc.pdf")
doc.file.download # => file contents
doc.file.attached? # => true
Implementation Details
The service implements all required Active Storage methods:
-
upload(key, io) - Upload file to SharePoint
-
download(key) - Download file from SharePoint
-
download_chunk(key, range) - Download partial file content
-
delete(key) - Delete file from SharePoint
-
exist?(key) - Check if file exists
-
url(key) - Get URL for blob
Key Points
-
File keys are mapped to blob filenames for better SharePoint organization
-
Automatic token refresh on 401 responses
-
Redirect following for CDN/Azure Blob Storage downloads
-
Signed URLs prevent 401 issues by routing through authenticated controller
-
Deferred deletion using PendingDelete registry
Error Handling
The service raises StandardError for invalid operations. Common errors:
-
“Failed to upload file to SharePoint” - Upload returned non-success status
-
“Failed to download file from SharePoint” - Download failed with error status
-
“Filename not found for key” - Blob deleted before file deletion from SharePoint
Performance Considerations
-
Tokens are cached and automatically refreshed before expiration
-
Chunked downloads supported for large files
-
Redirects followed to access CDN URLs without authorization issues
Instance Attribute Summary collapse
-
#auth ⇒ Authentication
readonly
Authentication handler.
-
#config ⇒ Configuration
readonly
SharePoint configuration.
-
#http ⇒ Http
readonly
HTTP request handler.
Instance Method Summary collapse
-
#delete(key) ⇒ Boolean
Delete a file from SharePoint.
-
#delete_prefixed(prefix) ⇒ void
Delete files matching a prefix (compatibility method).
-
#download(key) ⇒ String
Download a file from SharePoint.
-
#download_chunk(key, range) ⇒ String
Download a chunk (partial content) of a file from SharePoint.
-
#exist?(key) ⇒ Boolean
Check if a file exists in SharePoint.
-
#fetch_chunk(key, range) ⇒ Net::HTTPResponse
Fetch chunk response from SharePoint using HTTP Range header.
-
#fetch_download(key) ⇒ Net::HTTPResponse
Fetch the raw download response from SharePoint.
-
#handle_download_response(response) ⇒ String
Handle the HTTP response from a download request.
-
#initialize(**options) ⇒ SharepointService
constructor
Initialize the SharePoint storage service.
-
#upload(key, io) ⇒ void
Upload a file to SharePoint.
-
#url(key) ⇒ String
Get the URL for downloading a blob.
Constructor Details
#initialize(**options) ⇒ SharepointService
Initialize the SharePoint storage service
Creates configuration, authentication, and HTTP handler instances. The service is ready to use immediately after initialization.
122 123 124 125 126 |
# File 'lib/active_storage/service/sharepoint_service.rb', line 122 def initialize(**) # rubocop:disable Lint/MissingSuper @config = M365ActiveStorage::Configuration.new(**) @auth = M365ActiveStorage::Authentication.new(@config) @http = M365ActiveStorage::Http.new(@auth) end |
Instance Attribute Details
#auth ⇒ Authentication (readonly)
Authentication handler
95 96 97 |
# File 'lib/active_storage/service/sharepoint_service.rb', line 95 def auth @auth end |
#config ⇒ Configuration (readonly)
SharePoint configuration
95 96 97 |
# File 'lib/active_storage/service/sharepoint_service.rb', line 95 def config @config end |
#http ⇒ Http (readonly)
HTTP request handler
95 96 97 |
# File 'lib/active_storage/service/sharepoint_service.rb', line 95 def http @http end |
Instance Method Details
#delete(key) ⇒ Boolean
Delete a file from SharePoint
Removes a file from the SharePoint drive.
It prefers deletion by SharePoint item ID and falls back to filename when the item ID is not available. When the blob record has already been deleted, it reads pending data from PendingDelete.
287 288 289 290 291 292 293 294 295 |
# File 'lib/active_storage/service/sharepoint_service.rb', line 287 def delete(key) auth.ensure_valid_token delete_url = sharepoint_delete_url_for(key) raise "Identifier not found for key #{key}. Cannot delete file from SharePoint." unless delete_url response = http.delete(delete_url) response.code.to_i == 204 end |
#delete_prefixed(prefix) ⇒ void
This method returns an undefined value.
Delete files matching a prefix (compatibility method)
Retro compatibility method. Not implemented as the service works with individual keys rather than prefixes. Raises no error to maintain compatibility.
304 |
# File 'lib/active_storage/service/sharepoint_service.rb', line 304 def delete_prefixed(prefix); end |
#download(key) ⇒ String
Download a file from SharePoint
Retrieves the complete file content from SharePoint. Automatically retries once if the token expires (401 response).
176 177 178 179 180 181 182 183 184 |
# File 'lib/active_storage/service/sharepoint_service.rb', line 176 def download(key) response = fetch_download(key) if response.code.to_i == 401 # Token might have expired, force refresh and retry once auth.expire_token! response = fetch_download(key) end handle_download_response(response) end |
#download_chunk(key, range) ⇒ String
Download a chunk (partial content) of a file from SharePoint
Retrieves a specific byte range from a file, useful for large file streaming. Implements HTTP Range requests.
246 247 248 249 250 |
# File 'lib/active_storage/service/sharepoint_service.rb', line 246 def download_chunk(key, range) auth.ensure_valid_token response = fetch_chunk(key, range) handle_download_response(response) end |
#exist?(key) ⇒ Boolean
Check if a file exists in SharePoint
Queries SharePoint to check if a file with the given key exists.
315 316 317 318 319 320 |
# File 'lib/active_storage/service/sharepoint_service.rb', line 315 def exist?(key) auth.ensure_valid_token check_url = sharepoint_item_url_for(key) response = http.get(check_url) response.code.to_i == 200 end |
#fetch_chunk(key, range) ⇒ Net::HTTPResponse
Fetch chunk response from SharePoint using HTTP Range header
Internal method for making the HTTP Range request.
261 262 263 264 265 266 267 |
# File 'lib/active_storage/service/sharepoint_service.rb', line 261 def fetch_chunk(key, range) download_url = sharepoint_content_url_for(key) response = http.get(download_url, { "Range": "bytes=#{range.begin}-#{range.end}" }) return response unless should_retry_with_path_url?(key, response) http.get(legacy_content_url_for(key), { "Range": "bytes=#{range.begin}-#{range.end}" }) end |
#fetch_download(key) ⇒ Net::HTTPResponse
Fetch the raw download response from SharePoint
Internal method that makes the actual HTTP request for downloading a file.
194 195 196 197 198 199 200 201 |
# File 'lib/active_storage/service/sharepoint_service.rb', line 194 def fetch_download(key) auth.ensure_valid_token download_url = sharepoint_content_url_for(key) response = http.get(download_url) return response unless should_retry_with_path_url?(key, response) http.get(legacy_content_url_for(key)) end |
#handle_download_response(response) ⇒ String
Handle the HTTP response from a download request
Processes the response, following redirects if necessary (e.g., to CDN or Azure Blob Storage). Successful responses return the file content.
218 219 220 221 222 223 224 225 226 227 |
# File 'lib/active_storage/service/sharepoint_service.rb', line 218 def handle_download_response(response) case response.code.to_i when 200, 206 response.body when 302, 301 follow_redirect(response["location"]) else raise "Failed to download file from SharePoint: #{response.code}" end end |
#upload(key, io) ⇒ void
This method returns an undefined value.
Upload a file to SharePoint
Uploads file content to the configured SharePoint drive. The file is stored with the blob’s filename for better organization in SharePoint. The SharePoint item ID is persisted asynchronously in an after_commit hook to avoid being overwritten by Active Storage’s blob.save.
148 149 150 151 152 153 154 155 156 157 158 159 |
# File 'lib/active_storage/service/sharepoint_service.rb', line 148 def upload(key, io, **) auth.ensure_valid_token folder_path = sharepoint_folder_for(key) ensure_folder_path(folder_path) storage_path = build_storage_path(get_storage_name(key), folder_path) upload_url = "#{drive_url}/root:/#{encode_storage_path(storage_path)}:/content" response = http.put(upload_url, io.read, { "Content-Type": "application/octet-stream" }) raise "Failed to upload file to SharePoint" unless [201, 200].include?(response.code.to_i) # Store response body payload in thread-local storage for later retrieval in after_commit hook Thread.current[:sharepoint_upload_response] = response end |
#url(key) ⇒ String
Get the URL for downloading a blob
Returns a signed URL through the authenticated BlobsController rather than a direct SharePoint URL. This is necessary because direct SharePoint URLs require the Authorization header and would fail in browsers.
The URL includes the blob’s signed ID for security and the filename for reference.
339 340 341 342 343 344 345 346 347 |
# File 'lib/active_storage/service/sharepoint_service.rb', line 339 def url(key, **) # returns the path to the authenticated blob controller as direct sharepoint urls will fail with 401 # since its required the Authorization header to access the file # Find the blob and return the signed blob URL blob = ActiveStorage::Blob.find_by(key: key) # return a path to the authenticated blob controller "/rails/active_storage/blobs/#{blob.signed_id}/#{CGI.escape(blob.filename.to_s)}" end |