Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: use request interceptors in client service #11656

Merged
merged 2 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 42 additions & 12 deletions packages/web-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,44 +22,74 @@ $ yarn add @ownclouders/web-client

## Usage

To utilize the `web-client`, you must instantiate it with a base URI corresponding to your oCIS deployment and an axios instance. The axios instance, or at least its headers, is being used for all requests, requiring the inclusion of all relevant headers such as authorization.
### Graph

The graph client needs to be instantiated with a base URI corresponding to your oCIS deployment and an axios instance. The axios instance is being used for all requests, which means it needs to include all relevant headers either statically or via interceptor.

```
import axios from axios
import { client } from '@ownclouders/web-client'
import { graph } from '@ownclouders/web-client'

const accessToken = 'some_access_token'
const baseUri = 'some_base_uri'
const baseURI = 'some_base_uri'

const axiosClient = axios.create({
headers: { Authorization: accessToken }
})

const { graph, ocs, webdav } = client({ axiosClient, baseURI })
const graphClient = graph(baseURI, axiosClient)
```

### Graph

The following example demonstrates how to retrieve all spaces accessible to the user. A `SpaceResource` can then be used to e.g. fetch files and folders (see example down below).
The following example demonstrates how to retrieve all spaces accessible to the user. A `SpaceResource` can then be used to e.g. fetch files and folders (see webdav example down below).

```
const mySpaces = await graph.drives.listMyDrives()
const mySpaces = await graphClient.drives.listMyDrives()
```

### OCS

The ocs client needs to be instantiated with a base URI corresponding to your oCIS deployment and an axios instance. The axios instance is being used for all requests, which means it needs to include all relevant headers either statically or via interceptor.

```
import axios from axios
import { ocs } from '@ownclouders/web-client'

const accessToken = 'some_access_token'
const baseURI = 'some_base_uri'

const axiosClient = axios.create({
headers: { Authorization: accessToken }
})

const ocsClient = ocs(baseURI, axiosClient)
```

The following examples demonstrate how to fetch capabilities and sign URLs.

```
const capablities = await ocs.getCapabilities()
const capabilities = await ocsClient.getCapabilities()

const signedUrl = await ocs.signUrl('some_url_to_sign', 'your_username')
const signedUrl = await ocsClient.signUrl('some_url_to_sign', 'your_username')
```

### WebDAV
### WebDav

The webdav client needs to be instantiated with a base URI corresponding to your oCIS deployment. You can also pass a header callback which will be called with every dav request.

```
import { webdav } from '@ownclouders/web-client'

const accessToken = 'some_access_token'
const baseURI = 'some_base_uri'

const webDavClient = webdav(
baseURI,
() => ({ Authorization: accessToken })
)
```

The following example demonstrates how to list all resources of a given `SpaceResource` (see above how to fetch space resources).

```
const { resource, children } = await webdav.listFiles(spaceResource)
const { resource, children } = await webDavClient.listFiles(spaceResource)
```
17 changes: 16 additions & 1 deletion packages/web-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export * from './errors'
export * from './helpers'
export * from './utils'

export { graph, ocs, webdav }

interface Client {
graph: Graph
ocs: OCS
Expand All @@ -18,10 +20,23 @@ type ClientOptions = {
baseURI: string
}

/** @deprecated use `graph()`, `ocs()` and `webdav()` to initialize and use clients */
export const client = ({ axiosClient, baseURI }: ClientOptions): Client => {
const webDavHeaders = () => {
const authHeader = axiosClient.defaults?.headers?.Authorization
const languageHeader = axiosClient.defaults?.headers?.['Accept-Language']
const initiatorIdHeader = axiosClient.defaults?.headers?.['Initiator-ID']

return {
...(authHeader && { Authorization: authHeader.toString() }),
...(languageHeader && { 'Accept-Language': languageHeader.toString() }),
...(initiatorIdHeader && { 'Initiator-ID': initiatorIdHeader.toString() })
}
}

return {
graph: graph(baseURI, axiosClient),
ocs: ocs(baseURI, axiosClient),
webdav: webdav({ axiosClient, baseUrl: baseURI })
webdav: webdav(baseURI, webDavHeaders)
}
}
17 changes: 5 additions & 12 deletions packages/web-client/src/webdav/client/dav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,10 @@ import { buildPropFindBody, buildPropPatchBody } from './builders'
import { parseError, parseMultiStatus, parseTusHeaders } from './parsers'
import { WebDavResponseResource } from '../../helpers'
import { DavHttpError } from '../../errors'
import { AxiosInstance } from 'axios'

export interface DAVOptions {
axiosClient: AxiosInstance
baseUrl: string
headers?: () => Headers
}

export interface DavResult {
Expand All @@ -33,13 +32,13 @@ export type DAVRequestOptions = {

export class DAV {
private client: WebDAVClient
private axiosClient: AxiosInstance
private davPath: string
private headers: () => Headers

constructor({ axiosClient, baseUrl }: DAVOptions) {
constructor({ baseUrl, headers }: DAVOptions) {
this.davPath = urlJoin(baseUrl, 'remote.php/dav')
this.client = createClient(this.davPath, {})
this.axiosClient = axiosClient
this.headers = headers
}

public mkcol(path: string, opts: DAVRequestOptions = {}) {
Expand Down Expand Up @@ -175,17 +174,11 @@ export class DAV {
}

private buildHeaders(headers: Headers = {}): Headers {
const authHeader = this.axiosClient.defaults.headers.Authorization
const languageHeader = this.axiosClient.defaults.headers['Accept-Language']
const initiatorIdHeader = this.axiosClient.defaults.headers['Initiator-ID']

return {
...(authHeader && { Authorization: authHeader.toString() }),
...(languageHeader && { 'Accept-Language': languageHeader.toString() }),
...(initiatorIdHeader && { 'Initiator-ID': initiatorIdHeader.toString() }),
'Content-Type': 'application/xml; charset=utf-8',
'X-Requested-With': 'XMLHttpRequest',
'X-Request-ID': uuidV4(),
...(this.headers && { ...this.headers() }),
...headers
}
}
Expand Down
17 changes: 14 additions & 3 deletions packages/web-client/src/webdav/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { WebDAV, WebDavOptions } from './types'
import axios from 'axios'
import { Headers } from 'webdav'
import { WebDAV } from './types'
import { CopyFilesFactory } from './copyFiles'
import { CreateFolderFactory } from './createFolder'
import { GetFileContentsFactory } from './getFileContents'
Expand All @@ -25,8 +27,17 @@ export * from './types'
export type { ListFilesOptions, ListFilesResult } from './listFiles'
export type { GetFileContentsResponse } from './getFileContents'

export const webdav = (options: WebDavOptions): WebDAV => {
const dav = new DAV({ axiosClient: options.axiosClient, baseUrl: options.baseUrl })
export const webdav = (baseURI: string, headers?: () => Headers): WebDAV => {
const axiosClient = axios.create()
if (headers) {
axiosClient.interceptors.request.use((config) => {
Object.assign(config.headers, headers())
return config
})
}

const options = { axiosClient, baseUrl: baseURI, headers }
const dav = new DAV({ baseUrl: baseURI, headers })

const pathForFileIdFactory = GetPathForFileIdFactory(dav, options)
const { getPathForFileId } = pathForFileIdFactory
Expand Down
2 changes: 2 additions & 0 deletions packages/web-client/src/webdav/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ import { GetPathForFileIdFactory } from './getPathForFileId'
import { SetFavoriteFactory } from './setFavorite'
import { ListFavoriteFilesFactory } from './listFavoriteFiles'
import { AxiosInstance } from 'axios'
import { Headers } from 'webdav'

export interface WebDavOptions {
axiosClient: AxiosInstance
baseUrl: string
headers?: () => Headers
}

export interface WebDAV {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import axios, { type AxiosInstance } from 'axios'
import PQueue from 'p-queue'
import { type Resource, client, type SpaceResource } from '@ownclouders/web-client'
import { type Resource, webdav as _webdav, type SpaceResource } from '@ownclouders/web-client'
import type { DeleteWorkerTopic } from './useDeleteWorker'

type MessageData = {
Expand All @@ -17,26 +16,26 @@ type Message = {
data: MessageData
}

let axiosClient: AxiosInstance
let storedHeaders: Record<string, string>

self.onmessage = async (e: MessageEvent) => {
const { topic, data } = JSON.parse(e.data) as Message

if (topic === 'tokenUpdate' && axiosClient) {
const existingToken = axiosClient?.defaults.headers.Authorization
if (topic === 'tokenUpdate' && storedHeaders) {
const existingToken = storedHeaders.Authorization

// token must only be updated for bearer tokens, not on public links
if (existingToken?.toString().startsWith('Bearer')) {
Object.assign(axiosClient.defaults, { headers: { Authorization: data.accessToken } })
storedHeaders.Authorization = data.accessToken
}

return
}

const { baseUrl, headers, space, resources, concurrentRequests } = data

axiosClient = axios.create({ headers })
const { webdav } = client({ axiosClient, baseURI: baseUrl })
storedHeaders = headers
const webdav = _webdav(baseUrl, () => storedHeaders)

const successful: Resource[] = []
const failed: { resource: Resource; status: number }[] = []
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import axios, { type AxiosInstance } from 'axios'
import PQueue from 'p-queue'
import { join } from 'path'
import { type Resource, client } from '@ownclouders/web-client'
import { type Resource, webdav as _webdav } from '@ownclouders/web-client'
import type { WorkerTopic } from '../../piniaStores/webWorkers'
import { TransferType } from '../../../helpers/resource/conflictHandling/types'
import type { TransferData } from '../../../helpers/resource/conflictHandling'
Expand All @@ -18,26 +17,26 @@ type Message = {
data: MessageData
}

let axiosClient: AxiosInstance
let storedHeaders: Record<string, string>

self.onmessage = async (e: MessageEvent) => {
const { topic, data } = JSON.parse(e.data) as Message

if (topic === 'tokenUpdate' && axiosClient) {
const existingToken = axiosClient?.defaults.headers.Authorization
if (topic === 'tokenUpdate' && storedHeaders) {
const existingToken = storedHeaders.Authorization

// token must only be updated for bearer tokens, not on public links
if (existingToken?.toString().startsWith('Bearer')) {
Object.assign(axiosClient.defaults, { headers: { Authorization: data.accessToken } })
storedHeaders.Authorization = data.accessToken
}

return
}

const { baseUrl, headers, transferData } = data

axiosClient = axios.create({ headers })
const { webdav } = client({ axiosClient, baseURI: baseUrl })
storedHeaders = headers
const webdav = _webdav(baseUrl, () => storedHeaders)

const successful: Resource[] = []
const failed: { resourceName: string; error: Error }[] = []
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import axios, { type AxiosInstance } from 'axios'
import PQueue from 'p-queue'
import { dirname } from 'path'
import { client, urlJoin } from '@ownclouders/web-client'
import { webdav as _webdav, urlJoin } from '@ownclouders/web-client'
import type { Resource, SpaceResource } from '@ownclouders/web-client'
import type { WorkerTopic } from '../../piniaStores/webWorkers'
import type { WebDAV } from '@ownclouders/web-client/webdav'
Expand All @@ -21,7 +20,7 @@ type Message = {
data: MessageData
}

let axiosClient: AxiosInstance
let storedHeaders: Record<string, string>

const createFolderStructure = async ({
client,
Expand Down Expand Up @@ -60,21 +59,21 @@ const createFolderStructure = async ({
self.onmessage = async (e: MessageEvent) => {
const { topic, data } = JSON.parse(e.data) as Message

if (topic === 'tokenUpdate' && axiosClient) {
const existingToken = axiosClient?.defaults.headers.Authorization
if (topic === 'tokenUpdate' && storedHeaders) {
const existingToken = storedHeaders.Authorization

// token must only be updated for bearer tokens, not on public links
if (existingToken?.toString().startsWith('Bearer')) {
Object.assign(axiosClient.defaults, { headers: { Authorization: data.accessToken } })
storedHeaders.Authorization = data.accessToken
}

return
}

const { baseUrl, headers, space, resources, missingFolderPaths } = data

axiosClient = axios.create({ headers })
const { webdav } = client({ axiosClient, baseURI: baseUrl })
storedHeaders = headers
const webdav = _webdav(baseUrl, () => storedHeaders)

const successful: RestoreWorkerReturnData['successful'] = []
const failed: RestoreWorkerReturnData['failed'] = []
Expand Down
18 changes: 16 additions & 2 deletions packages/web-pkg/src/http/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, CancelTokenSource } from 'axios'
import axios, {
AxiosInstance,
AxiosRequestConfig,
AxiosResponse,
CancelTokenSource,
InternalAxiosRequestConfig
} from 'axios'
import merge from 'lodash-es/merge'
import { z } from 'zod'

Expand All @@ -9,9 +15,17 @@ export class HttpClient {
private readonly instance: AxiosInstance
private readonly cancelToken: CancelTokenSource

constructor(config?: AxiosRequestConfig) {
constructor(
config?: AxiosRequestConfig,
interceptor?: (
value: InternalAxiosRequestConfig<any>
) => InternalAxiosRequestConfig<any> | Promise<InternalAxiosRequestConfig<any>>
) {
this.cancelToken = axios.CancelToken.source()
this.instance = axios.create(config)
if (interceptor) {
this.instance.interceptors.request.use(interceptor)
}
}

public cancel(msg?: string): void {
Expand Down
Loading