Class: Service::SharepointService

Inherits:
Service
  • Object
show all
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

Instance Method Summary collapse

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.

Examples:

service = ActiveStorage::Service::SharepointService.new(
  ms_graph_url: "https://graph.microsoft.com",
  # ... other required params
)

Parameters:

  • options (Hash)

    Configuration options (passed to Configuration)

Options Hash (**options):

  • :ms_graph_url (String)

    The Microsoft Graph API URL

  • :ms_graph_version (String)

    The Graph API version

  • :auth_host (String)

    The OAuth2 host

  • :tenant_id (String)

    Azure AD tenant ID

  • :app_id (String)

    Azure AD application ID

  • :secret (String)

    Azure AD client secret

  • :site_id (String)

    SharePoint site ID

  • :drive_id (String)

    SharePoint drive ID

Raises:

  • (KeyError)

    if required configuration is missing

See Also:



122
123
124
125
126
# File 'lib/active_storage/service/sharepoint_service.rb', line 122

def initialize(**options) # rubocop:disable Lint/MissingSuper
  @config = M365ActiveStorage::Configuration.new(**options)
  @auth = M365ActiveStorage::Authentication.new(@config)
  @http = M365ActiveStorage::Http.new(@auth)
end

Instance Attribute Details

#authAuthentication (readonly)

Authentication handler

Returns:

  • (Authentication)

    the current value of auth



95
96
97
# File 'lib/active_storage/service/sharepoint_service.rb', line 95

def auth
  @auth
end

#configConfiguration (readonly)

SharePoint configuration

Returns:

  • (Configuration)

    the current value of config



95
96
97
# File 'lib/active_storage/service/sharepoint_service.rb', line 95

def config
  @config
end

#httpHttp (readonly)

HTTP request handler

Returns:

  • (Http)

    the current value of http



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.

Examples:

success = service.delete("key123")  # => true

Parameters:

  • key (String)

    The blob key to delete

Returns:

  • (Boolean)

    true if deletion was successful (204 response)

Raises:

  • (StandardError)

    if identifier not found or deletion fails

See Also:



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.

Parameters:

  • prefix (String)

    The prefix to match (unused)



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).

Examples:

content = service.download("key123")  # => file contents

Parameters:

  • key (String)

    The blob key to download

Returns:

  • (String)

    The file content

Raises:

  • (StandardError)

    if download fails

See Also:



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.

Examples:

# Download first 1MB of a file
chunk = service.download_chunk("key123", 0..(1024*1024-1))

Parameters:

  • key (String)

    The blob key to download from

  • range (Range)

    The byte range to retrieve (e.g., 0..1023)

Returns:

  • (String)

    The requested file chunk

Raises:

  • (StandardError)

    if download fails

See Also:



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.

Examples:

service.exist?("key123")  # => true or false

Parameters:

  • key (String)

    The blob key to check

Returns:

  • (Boolean)

    true if the file exists, false otherwise



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.

Parameters:

  • key (String)

    The blob key

  • range (Range)

    The byte range to retrieve

Returns:

  • (Net::HTTPResponse)

    The HTTP response

See Also:



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.

Parameters:

  • key (String)

    The blob key to download

Returns:

  • (Net::HTTPResponse)

    The HTTP response

See Also:



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.

Examples:

response = http.get(url)
content = handle_download_response(response)  # => file contents

Parameters:

  • response (Net::HTTPResponse)

    The HTTP response from SharePoint

Returns:

  • (String)

    The file content

Raises:

  • (StandardError)

    if response code indicates an error

See Also:

  • #follow_redirect


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.

Examples:

file = File.open("document.pdf")
service.upload("key123", file)  # File now in SharePoint

Parameters:

  • key (String)

    The Active Storage blob key (ignored, filename used instead)

  • io (IO)

    The file content as an IO object

Raises:

  • (StandardError)

    if upload fails

See Also:

  • #get_storage_name
  • #handle_upload_response
  • #ensure_folder_path


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.

Examples:

url = service.url("key123")
# => "/rails/active_storage/blobs/signed_id/document.pdf"

Parameters:

  • key (String)

    The blob key to get the URL for

Returns:

  • (String)

    A path to the authenticated blob download action

See Also:



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