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. Requires the filename to be available from the PendingDelete registry (set before the blob was deleted).

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 filename not found or deletion fails

See Also:



272
273
274
275
276
277
278
279
280
281
282
# File 'lib/active_storage/service/sharepoint_service.rb', line 272

def delete(key)
  auth.ensure_valid_token
  # get the filename from the pending deletes storage
  # because once the blob is deleted, we can no longer get the filename from the blob record
  storage_name = M365ActiveStorage::PendingDelete.get(key)
  raise "Filename not found for key #{key}. Cannot delete file from SharePoint." unless storage_name

  delete_url = "#{drive_url}/root:/#{CGI.escape(storage_name)}"
  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)



291
# File 'lib/active_storage/service/sharepoint_service.rb', line 291

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:



168
169
170
171
172
173
174
175
176
# File 'lib/active_storage/service/sharepoint_service.rb', line 168

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:



236
237
238
239
240
# File 'lib/active_storage/service/sharepoint_service.rb', line 236

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



302
303
304
305
306
307
308
# File 'lib/active_storage/service/sharepoint_service.rb', line 302

def exist?(key)
  auth.ensure_valid_token
  storage_name = get_storage_name(key)
  check_url = "#{drive_url}/root:/#{CGI.escape(storage_name)}"
  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:



251
252
253
254
255
# File 'lib/active_storage/service/sharepoint_service.rb', line 251

def fetch_chunk(key, range)
  storage_name = get_storage_name(key)
  download_url = "#{drive_url}/root:/#{CGI.escape(storage_name)}:/content"
  http.get(download_url, { "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:



186
187
188
189
190
191
# File 'lib/active_storage/service/sharepoint_service.rb', line 186

def fetch_download(key)
  auth.ensure_valid_token
  storage_name = get_storage_name(key)
  download_url = "#{drive_url}/root:/#{CGI.escape(storage_name)}:/content"
  http.get(download_url)
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


208
209
210
211
212
213
214
215
216
217
# File 'lib/active_storage/service/sharepoint_service.rb', line 208

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.

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


145
146
147
148
149
150
151
# File 'lib/active_storage/service/sharepoint_service.rb', line 145

def upload(key, io, **)
  auth.ensure_valid_token
  storage_name = get_storage_name(key)
  upload_url = "#{drive_url}/root:/#{CGI.escape(storage_name)}:/content"
  response = http.put(upload_url, io.read, { "Content-Type": "application/octet-stream" })
  handle_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:



327
328
329
330
331
332
333
334
335
# File 'lib/active_storage/service/sharepoint_service.rb', line 327

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