mirror of
https://github.com/rclone/rclone.git
synced 2024-12-22 23:22:08 +01:00
serve: add dlna server
This commit is contained in:
parent
5edfd31a6d
commit
0b7fdf16a2
451
cmd/serve/dlna/cd-service-desc.go
Normal file
451
cmd/serve/dlna/cd-service-desc.go
Normal file
@ -0,0 +1,451 @@
|
|||||||
|
package dlna
|
||||||
|
|
||||||
|
const contentDirectoryServiceDescription = `<?xml version="1.0"?>
|
||||||
|
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
|
||||||
|
<specVersion>
|
||||||
|
<major>1</major>
|
||||||
|
<minor>0</minor>
|
||||||
|
</specVersion>
|
||||||
|
<actionList>
|
||||||
|
<action>
|
||||||
|
<name>GetSearchCapabilities</name>
|
||||||
|
<argumentList>
|
||||||
|
<argument>
|
||||||
|
<name>SearchCaps</name>
|
||||||
|
<direction>out</direction>
|
||||||
|
<relatedStateVariable>SearchCapabilities</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
</argumentList>
|
||||||
|
</action>
|
||||||
|
<action>
|
||||||
|
<name>GetSortCapabilities</name>
|
||||||
|
<argumentList>
|
||||||
|
<argument>
|
||||||
|
<name>SortCaps</name>
|
||||||
|
<direction>out</direction>
|
||||||
|
<relatedStateVariable>SortCapabilities</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
</argumentList>
|
||||||
|
</action>
|
||||||
|
<action>
|
||||||
|
<name>GetSortExtensionCapabilities</name>
|
||||||
|
<argumentList>
|
||||||
|
<argument>
|
||||||
|
<name>SortExtensionCaps</name>
|
||||||
|
<direction>out</direction>
|
||||||
|
<relatedStateVariable>SortExtensionCapabilities</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
</argumentList>
|
||||||
|
</action>
|
||||||
|
<action>
|
||||||
|
<name>GetFeatureList</name>
|
||||||
|
<argumentList>
|
||||||
|
<argument>
|
||||||
|
<name>FeatureList</name>
|
||||||
|
<direction>out</direction>
|
||||||
|
<relatedStateVariable>FeatureList</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
</argumentList>
|
||||||
|
</action>
|
||||||
|
<action>
|
||||||
|
<name>GetSystemUpdateID</name>
|
||||||
|
<argumentList>
|
||||||
|
<argument>
|
||||||
|
<name>Id</name>
|
||||||
|
<direction>out</direction>
|
||||||
|
<relatedStateVariable>SystemUpdateID</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
</argumentList>
|
||||||
|
</action>
|
||||||
|
<action>
|
||||||
|
<name>Browse</name>
|
||||||
|
<argumentList>
|
||||||
|
<argument>
|
||||||
|
<name>ObjectID</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_ObjectID</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>BrowseFlag</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_BrowseFlag</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>Filter</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_Filter</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>StartingIndex</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_Index</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>RequestedCount</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_Count</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>SortCriteria</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_SortCriteria</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>Result</name>
|
||||||
|
<direction>out</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_Result</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>NumberReturned</name>
|
||||||
|
<direction>out</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_Count</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>TotalMatches</name>
|
||||||
|
<direction>out</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_Count</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>UpdateID</name>
|
||||||
|
<direction>out</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_UpdateID</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
</argumentList>
|
||||||
|
</action>
|
||||||
|
<action>
|
||||||
|
<name>Search</name>
|
||||||
|
<argumentList>
|
||||||
|
<argument>
|
||||||
|
<name>ContainerID</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_ObjectID</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>SearchCriteria</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_SearchCriteria</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>Filter</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_Filter</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>StartingIndex</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_Index</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>RequestedCount</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_Count</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>SortCriteria</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_SortCriteria</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>Result</name>
|
||||||
|
<direction>out</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_Result</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>NumberReturned</name>
|
||||||
|
<direction>out</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_Count</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>TotalMatches</name>
|
||||||
|
<direction>out</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_Count</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>UpdateID</name>
|
||||||
|
<direction>out</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_UpdateID</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
</argumentList>
|
||||||
|
</action>
|
||||||
|
<action>
|
||||||
|
<name>CreateObject</name>
|
||||||
|
<argumentList>
|
||||||
|
<argument>
|
||||||
|
<name>ContainerID</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_ObjectID</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>Elements</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_Result</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>ObjectID</name>
|
||||||
|
<direction>out</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_ObjectID</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>Result</name>
|
||||||
|
<direction>out</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_Result</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
</argumentList>
|
||||||
|
</action>
|
||||||
|
<action>
|
||||||
|
<name>DestroyObject</name>
|
||||||
|
<argumentList>
|
||||||
|
<argument>
|
||||||
|
<name>ObjectID</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_ObjectID</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
</argumentList>
|
||||||
|
</action>
|
||||||
|
<action>
|
||||||
|
<name>UpdateObject</name>
|
||||||
|
<argumentList>
|
||||||
|
<argument>
|
||||||
|
<name>ObjectID</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_ObjectID</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>CurrentTagValue</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_TagValueList</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>NewTagValue</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_TagValueList</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
</argumentList>
|
||||||
|
</action>
|
||||||
|
<action>
|
||||||
|
<name>MoveObject</name>
|
||||||
|
<argumentList>
|
||||||
|
<argument>
|
||||||
|
<name>ObjectID</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_ObjectID</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>NewParentID</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_ObjectID</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>NewObjectID</name>
|
||||||
|
<direction>out</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_ObjectID</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
</argumentList>
|
||||||
|
</action>
|
||||||
|
<action>
|
||||||
|
<name>ImportResource</name>
|
||||||
|
<argumentList>
|
||||||
|
<argument>
|
||||||
|
<name>SourceURI</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_URI</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>DestinationURI</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_URI</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>TransferID</name>
|
||||||
|
<direction>out</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_TransferID</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
</argumentList>
|
||||||
|
</action>
|
||||||
|
<action>
|
||||||
|
<name>ExportResource</name>
|
||||||
|
<argumentList>
|
||||||
|
<argument>
|
||||||
|
<name>SourceURI</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_URI</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>DestinationURI</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_URI</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>TransferID</name>
|
||||||
|
<direction>out</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_TransferID</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
</argumentList>
|
||||||
|
</action>
|
||||||
|
<action>
|
||||||
|
<name>StopTransferResource</name>
|
||||||
|
<argumentList>
|
||||||
|
<argument>
|
||||||
|
<name>TransferID</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_TransferID</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
</argumentList>
|
||||||
|
</action>
|
||||||
|
<action>
|
||||||
|
<name>DeleteResource</name>
|
||||||
|
<argumentList>
|
||||||
|
<argument>
|
||||||
|
<name>ResourceURI</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_URI</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
</argumentList>
|
||||||
|
</action>
|
||||||
|
<action>
|
||||||
|
<name>GetTransferProgress</name>
|
||||||
|
<argumentList>
|
||||||
|
<argument>
|
||||||
|
<name>TransferID</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_TransferID</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>TransferStatus</name>
|
||||||
|
<direction>out</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_TransferStatus</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>TransferLength</name>
|
||||||
|
<direction>out</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_TransferLength</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>TransferTotal</name>
|
||||||
|
<direction>out</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_TransferTotal</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
</argumentList>
|
||||||
|
</action>
|
||||||
|
<action>
|
||||||
|
<name>CreateReference</name>
|
||||||
|
<argumentList>
|
||||||
|
<argument>
|
||||||
|
<name>ContainerID</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_ObjectID</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>ObjectID</name>
|
||||||
|
<direction>in</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_ObjectID</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<name>NewID</name>
|
||||||
|
<direction>out</direction>
|
||||||
|
<relatedStateVariable>A_ARG_TYPE_ObjectID</relatedStateVariable>
|
||||||
|
</argument>
|
||||||
|
</argumentList>
|
||||||
|
</action>
|
||||||
|
</actionList>
|
||||||
|
<serviceStateTable>
|
||||||
|
<stateVariable sendEvents="no">
|
||||||
|
<name>SearchCapabilities</name>
|
||||||
|
<dataType>string</dataType>
|
||||||
|
</stateVariable>
|
||||||
|
<stateVariable sendEvents="no">
|
||||||
|
<name>SortCapabilities</name>
|
||||||
|
<dataType>string</dataType>
|
||||||
|
</stateVariable>
|
||||||
|
<stateVariable sendEvents="no">
|
||||||
|
<name>SortExtensionCapabilities</name>
|
||||||
|
<dataType>string</dataType>
|
||||||
|
</stateVariable>
|
||||||
|
<stateVariable sendEvents="yes">
|
||||||
|
<name>SystemUpdateID</name>
|
||||||
|
<dataType>ui4</dataType>
|
||||||
|
</stateVariable>
|
||||||
|
<stateVariable sendEvents="yes">
|
||||||
|
<name>ContainerUpdateIDs</name>
|
||||||
|
<dataType>string</dataType>
|
||||||
|
</stateVariable>
|
||||||
|
<stateVariable sendEvents="yes">
|
||||||
|
<name>TransferIDs</name>
|
||||||
|
<dataType>string</dataType>
|
||||||
|
</stateVariable>
|
||||||
|
<stateVariable sendEvents="no">
|
||||||
|
<name>FeatureList</name>
|
||||||
|
<dataType>string</dataType>
|
||||||
|
</stateVariable>
|
||||||
|
<stateVariable sendEvents="no">
|
||||||
|
<name>A_ARG_TYPE_ObjectID</name>
|
||||||
|
<dataType>string</dataType>
|
||||||
|
</stateVariable>
|
||||||
|
<stateVariable sendEvents="no">
|
||||||
|
<name>A_ARG_TYPE_Result</name>
|
||||||
|
<dataType>string</dataType>
|
||||||
|
</stateVariable>
|
||||||
|
<stateVariable sendEvents="no">
|
||||||
|
<name>A_ARG_TYPE_SearchCriteria</name>
|
||||||
|
<dataType>string</dataType>
|
||||||
|
</stateVariable>
|
||||||
|
<stateVariable sendEvents="no">
|
||||||
|
<name>A_ARG_TYPE_BrowseFlag</name>
|
||||||
|
<dataType>string</dataType>
|
||||||
|
<allowedValueList>
|
||||||
|
<allowedValue>BrowseMetadata</allowedValue>
|
||||||
|
<allowedValue>BrowseDirectChildren</allowedValue>
|
||||||
|
</allowedValueList>
|
||||||
|
</stateVariable>
|
||||||
|
<stateVariable sendEvents="no">
|
||||||
|
<name>A_ARG_TYPE_Filter</name>
|
||||||
|
<dataType>string</dataType>
|
||||||
|
</stateVariable>
|
||||||
|
<stateVariable sendEvents="no">
|
||||||
|
<name>A_ARG_TYPE_SortCriteria</name>
|
||||||
|
<dataType>string</dataType>
|
||||||
|
</stateVariable>
|
||||||
|
<stateVariable sendEvents="no">
|
||||||
|
<name>A_ARG_TYPE_Index</name>
|
||||||
|
<dataType>ui4</dataType>
|
||||||
|
</stateVariable>
|
||||||
|
<stateVariable sendEvents="no">
|
||||||
|
<name>A_ARG_TYPE_Count</name>
|
||||||
|
<dataType>ui4</dataType>
|
||||||
|
</stateVariable>
|
||||||
|
<stateVariable sendEvents="no">
|
||||||
|
<name>A_ARG_TYPE_UpdateID</name>
|
||||||
|
<dataType>ui4</dataType>
|
||||||
|
</stateVariable>
|
||||||
|
<stateVariable sendEvents="no">
|
||||||
|
<name>A_ARG_TYPE_TransferID</name>
|
||||||
|
<dataType>ui4</dataType>
|
||||||
|
</stateVariable>
|
||||||
|
<stateVariable sendEvents="no">
|
||||||
|
<name>A_ARG_TYPE_TransferStatus</name>
|
||||||
|
<dataType>string</dataType>
|
||||||
|
<allowedValueList>
|
||||||
|
<allowedValue>COMPLETED</allowedValue>
|
||||||
|
<allowedValue>ERROR</allowedValue>
|
||||||
|
<allowedValue>IN_PROGRESS</allowedValue>
|
||||||
|
<allowedValue>STOPPED</allowedValue>
|
||||||
|
</allowedValueList>
|
||||||
|
</stateVariable>
|
||||||
|
<stateVariable sendEvents="no">
|
||||||
|
<name>A_ARG_TYPE_TransferLength</name>
|
||||||
|
<dataType>string</dataType>
|
||||||
|
</stateVariable>
|
||||||
|
<stateVariable sendEvents="no">
|
||||||
|
<name>A_ARG_TYPE_TransferTotal</name>
|
||||||
|
<dataType>string</dataType>
|
||||||
|
</stateVariable>
|
||||||
|
<stateVariable sendEvents="no">
|
||||||
|
<name>A_ARG_TYPE_TagValueList</name>
|
||||||
|
<dataType>string</dataType>
|
||||||
|
</stateVariable>
|
||||||
|
<stateVariable sendEvents="no">
|
||||||
|
<name>A_ARG_TYPE_URI</name>
|
||||||
|
<dataType>uri</dataType>
|
||||||
|
</stateVariable>
|
||||||
|
</serviceStateTable>
|
||||||
|
</scpd>`
|
240
cmd/serve/dlna/cds.go
Normal file
240
cmd/serve/dlna/cds.go
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
package dlna
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/anacrolix/dms/dlna"
|
||||||
|
"github.com/anacrolix/dms/upnp"
|
||||||
|
"github.com/anacrolix/dms/upnpav"
|
||||||
|
"github.com/ncw/rclone/vfs"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type contentDirectoryService struct {
|
||||||
|
*server
|
||||||
|
upnp.Eventing
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cds *contentDirectoryService) updateIDString() string {
|
||||||
|
return fmt.Sprintf("%d", uint32(os.Getpid()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turns the given entry and DMS host into a UPnP object. A nil object is
|
||||||
|
// returned if the entry is not of interest.
|
||||||
|
func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, fileInfo os.FileInfo, host string) (ret interface{}, err error) {
|
||||||
|
obj := upnpav.Object{
|
||||||
|
ID: cdsObject.ID(),
|
||||||
|
Restricted: 1,
|
||||||
|
ParentID: cdsObject.ParentID(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileInfo.IsDir() {
|
||||||
|
obj.Class = "object.container.storageFolder"
|
||||||
|
obj.Title = fileInfo.Name()
|
||||||
|
ret = upnpav.Container{Object: obj}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fileInfo.Mode().IsRegular() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hardcode "videoItem" so that files show up in VLC.
|
||||||
|
obj.Class = "object.item.videoItem"
|
||||||
|
obj.Title = fileInfo.Name()
|
||||||
|
|
||||||
|
item := upnpav.Item{
|
||||||
|
Object: obj,
|
||||||
|
Res: make([]upnpav.Resource, 0, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
item.Res = append(item.Res, upnpav.Resource{
|
||||||
|
URL: (&url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: host,
|
||||||
|
Path: resPath,
|
||||||
|
RawQuery: url.Values{
|
||||||
|
"path": {cdsObject.Path},
|
||||||
|
}.Encode(),
|
||||||
|
}).String(),
|
||||||
|
// Hardcode "video/x-matroska" so that files show up in VLC.
|
||||||
|
ProtocolInfo: fmt.Sprintf("http-get:*:video/x-matroska:%s", dlna.ContentFeatures{
|
||||||
|
SupportRange: true,
|
||||||
|
}.String()),
|
||||||
|
Bitrate: 0,
|
||||||
|
Duration: "",
|
||||||
|
Size: uint64(fileInfo.Size()),
|
||||||
|
Resolution: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
ret = item
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns all the upnpav objects in a directory.
|
||||||
|
func (cds *contentDirectoryService) readContainer(o object, host string) (ret []interface{}, err error) {
|
||||||
|
node, err := cds.vfs.Stat(o.Path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !node.IsDir() {
|
||||||
|
err = errors.New("not a directory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := node.(*vfs.Dir)
|
||||||
|
dirEntries, err := dir.ReadDirAll()
|
||||||
|
if err != nil {
|
||||||
|
err = errors.New("failed to list directory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(dirEntries)
|
||||||
|
|
||||||
|
for _, de := range dirEntries {
|
||||||
|
child := object{
|
||||||
|
path.Join(o.Path, de.Name()),
|
||||||
|
}
|
||||||
|
obj, err := cds.cdsObjectToUpnpavObject(child, de, host)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error with %s: %s", child.FilePath(), err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if obj != nil {
|
||||||
|
ret = append(ret, obj)
|
||||||
|
} else {
|
||||||
|
log.Printf("bad %s", de)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type browse struct {
|
||||||
|
ObjectID string
|
||||||
|
BrowseFlag string
|
||||||
|
Filter string
|
||||||
|
StartingIndex int
|
||||||
|
RequestedCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContentDirectory object from ObjectID.
|
||||||
|
func (cds *contentDirectoryService) objectFromID(id string) (o object, err error) {
|
||||||
|
o.Path, err = url.QueryUnescape(id)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if o.Path == "0" {
|
||||||
|
o.Path = "/"
|
||||||
|
}
|
||||||
|
o.Path = path.Clean(o.Path)
|
||||||
|
if !path.IsAbs(o.Path) {
|
||||||
|
err = fmt.Errorf("bad ObjectID %v", o.Path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cds *contentDirectoryService) Handle(action string, argsXML []byte, r *http.Request) (map[string]string, error) {
|
||||||
|
host := r.Host
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "GetSystemUpdateID":
|
||||||
|
return map[string]string{
|
||||||
|
"Id": cds.updateIDString(),
|
||||||
|
}, nil
|
||||||
|
case "GetSortCapabilities":
|
||||||
|
return map[string]string{
|
||||||
|
"SortCaps": "dc:title",
|
||||||
|
}, nil
|
||||||
|
case "Browse":
|
||||||
|
var browse browse
|
||||||
|
if err := xml.Unmarshal([]byte(argsXML), &browse); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
obj, err := cds.objectFromID(browse.ObjectID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, upnp.Errorf(upnpav.NoSuchObjectErrorCode, err.Error())
|
||||||
|
}
|
||||||
|
switch browse.BrowseFlag {
|
||||||
|
case "BrowseDirectChildren":
|
||||||
|
objs, err := cds.readContainer(obj, host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, upnp.Errorf(upnpav.NoSuchObjectErrorCode, err.Error())
|
||||||
|
}
|
||||||
|
totalMatches := len(objs)
|
||||||
|
objs = objs[func() (low int) {
|
||||||
|
low = browse.StartingIndex
|
||||||
|
if low > len(objs) {
|
||||||
|
low = len(objs)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}():]
|
||||||
|
if browse.RequestedCount != 0 && int(browse.RequestedCount) < len(objs) {
|
||||||
|
objs = objs[:browse.RequestedCount]
|
||||||
|
}
|
||||||
|
result, err := xml.Marshal(objs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return map[string]string{
|
||||||
|
"TotalMatches": fmt.Sprint(totalMatches),
|
||||||
|
"NumberReturned": fmt.Sprint(len(objs)),
|
||||||
|
"Result": didlLite(string(result)),
|
||||||
|
"UpdateID": cds.updateIDString(),
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
return nil, upnp.Errorf(upnp.ArgumentValueInvalidErrorCode, "unhandled browse flag: %v", browse.BrowseFlag)
|
||||||
|
}
|
||||||
|
case "GetSearchCapabilities":
|
||||||
|
return map[string]string{
|
||||||
|
"SearchCaps": "",
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
return nil, upnp.InvalidActionError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Represents a ContentDirectory object.
|
||||||
|
type object struct {
|
||||||
|
Path string // The cleaned, absolute path for the object relative to the server.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the actual local filesystem path for the object.
|
||||||
|
func (o *object) FilePath() string {
|
||||||
|
return filepath.FromSlash(o.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the ObjectID for the object. This is used in various ContentDirectory actions.
|
||||||
|
func (o object) ID() string {
|
||||||
|
if !path.IsAbs(o.Path) {
|
||||||
|
log.Panicf("Relative object path: %s", o.Path)
|
||||||
|
}
|
||||||
|
if len(o.Path) == 1 {
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
return url.QueryEscape(o.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *object) IsRoot() bool {
|
||||||
|
return o.Path == "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the object's parent ObjectID. Fortunately it can be deduced from the
|
||||||
|
// ObjectID (for now).
|
||||||
|
func (o object) ParentID() string {
|
||||||
|
if o.IsRoot() {
|
||||||
|
return "-1"
|
||||||
|
}
|
||||||
|
o.Path = path.Dir(o.Path)
|
||||||
|
return o.ID()
|
||||||
|
}
|
440
cmd/serve/dlna/dlna.go
Normal file
440
cmd/serve/dlna/dlna.go
Normal file
@ -0,0 +1,440 @@
|
|||||||
|
package dlna
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/anacrolix/dms/soap"
|
||||||
|
"github.com/anacrolix/dms/ssdp"
|
||||||
|
"github.com/anacrolix/dms/upnp"
|
||||||
|
"github.com/ncw/rclone/cmd"
|
||||||
|
"github.com/ncw/rclone/cmd/serve/dlna/dlnaflags"
|
||||||
|
"github.com/ncw/rclone/fs"
|
||||||
|
"github.com/ncw/rclone/vfs"
|
||||||
|
"github.com/ncw/rclone/vfs/vfsflags"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
dlnaflags.AddFlags(Command.Flags())
|
||||||
|
vfsflags.AddFlags(Command.Flags())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command definition for cobra.
|
||||||
|
var Command = &cobra.Command{
|
||||||
|
Use: "dlna remote:path",
|
||||||
|
Short: `Serve remote:path over DLNA`,
|
||||||
|
Long: `rclone serve dlna is a DLNA media server for media stored in a rclone remote. Many
|
||||||
|
devices, such as the Xbox and PlayStation, can automatically discover this server in the LAN
|
||||||
|
and play audio/video from it. VLC is also supported. Service discovery uses UDP multicast
|
||||||
|
packets (SSDP) and will thus only work on LANs.
|
||||||
|
|
||||||
|
Rclone will list all files present in the remote, without filtering based on media formats or
|
||||||
|
file extensions. Additionally, there is no media transcoding support. This means that some
|
||||||
|
players might show files that they are not able to play back correctly.
|
||||||
|
|
||||||
|
` + dlnaflags.Help + vfs.Help,
|
||||||
|
Run: func(command *cobra.Command, args []string) {
|
||||||
|
cmd.CheckArgs(1, 1, command, args)
|
||||||
|
f := cmd.NewFsSrc(args)
|
||||||
|
|
||||||
|
cmd.Run(false, false, command, func() error {
|
||||||
|
s := newServer(f, &dlnaflags.Opt)
|
||||||
|
if err := s.Serve(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
s.Wait()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
serverField = "Linux/3.4 DLNADOC/1.50 UPnP/1.0 DMS/1.0"
|
||||||
|
rootDeviceType = "urn:schemas-upnp-org:device:MediaServer:1"
|
||||||
|
rootDeviceModelName = "rclone"
|
||||||
|
resPath = "/res"
|
||||||
|
rootDescPath = "/rootDesc.xml"
|
||||||
|
serviceControlURL = "/ctl"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Groups the service definition with its XML description.
|
||||||
|
type service struct {
|
||||||
|
upnp.Service
|
||||||
|
SCPD string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exposed UPnP AV services.
|
||||||
|
var services = []*service{
|
||||||
|
{
|
||||||
|
Service: upnp.Service{
|
||||||
|
ServiceType: "urn:schemas-upnp-org:service:ContentDirectory:1",
|
||||||
|
ServiceId: "urn:upnp-org:serviceId:ContentDirectory",
|
||||||
|
ControlURL: serviceControlURL,
|
||||||
|
},
|
||||||
|
SCPD: contentDirectoryServiceDescription,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func devices() []string {
|
||||||
|
return []string{
|
||||||
|
"urn:schemas-upnp-org:device:MediaServer:1",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func serviceTypes() (ret []string) {
|
||||||
|
for _, s := range services {
|
||||||
|
ret = append(ret, s.ServiceType)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type server struct {
|
||||||
|
// The service SOAP handler keyed by service URN.
|
||||||
|
services map[string]UPnPService
|
||||||
|
|
||||||
|
Interfaces []net.Interface
|
||||||
|
|
||||||
|
HTTPConn net.Listener
|
||||||
|
httpListenAddr string
|
||||||
|
httpServeMux *http.ServeMux
|
||||||
|
|
||||||
|
rootDeviceUUID string
|
||||||
|
rootDescXML []byte
|
||||||
|
|
||||||
|
FriendlyName string
|
||||||
|
|
||||||
|
// For waiting on the listener to close
|
||||||
|
waitChan chan struct{}
|
||||||
|
|
||||||
|
// Time interval between SSPD announces
|
||||||
|
AnnounceInterval time.Duration
|
||||||
|
|
||||||
|
f fs.Fs
|
||||||
|
vfs *vfs.VFS
|
||||||
|
}
|
||||||
|
|
||||||
|
func newServer(f fs.Fs, opt *dlnaflags.Options) *server {
|
||||||
|
hostName, err := os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
hostName = ""
|
||||||
|
} else {
|
||||||
|
hostName = " (" + hostName + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &server{
|
||||||
|
AnnounceInterval: 10 * time.Second,
|
||||||
|
FriendlyName: "rclone" + hostName,
|
||||||
|
|
||||||
|
httpListenAddr: opt.ListenAddr,
|
||||||
|
|
||||||
|
f: f,
|
||||||
|
vfs: vfs.New(f, &vfsflags.Opt),
|
||||||
|
}
|
||||||
|
|
||||||
|
s.initServicesMap()
|
||||||
|
s.listInterfaces()
|
||||||
|
|
||||||
|
s.httpServeMux = http.NewServeMux()
|
||||||
|
s.rootDeviceUUID = makeDeviceUUID(s.FriendlyName)
|
||||||
|
s.rootDescXML, err = xml.MarshalIndent(
|
||||||
|
upnp.DeviceDesc{
|
||||||
|
SpecVersion: upnp.SpecVersion{Major: 1, Minor: 0},
|
||||||
|
Device: upnp.Device{
|
||||||
|
DeviceType: rootDeviceType,
|
||||||
|
FriendlyName: s.FriendlyName,
|
||||||
|
Manufacturer: "rclone (rclone.org)",
|
||||||
|
ModelName: rootDeviceModelName,
|
||||||
|
UDN: s.rootDeviceUUID,
|
||||||
|
ServiceList: func() (ss []upnp.Service) {
|
||||||
|
for _, s := range services {
|
||||||
|
ss = append(ss, s.Service)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
" ", " ")
|
||||||
|
if err != nil {
|
||||||
|
// Contents are hardcoded, so this will never happen in production.
|
||||||
|
log.Panicf("Marshal root descriptor XML: %v", err)
|
||||||
|
}
|
||||||
|
s.rootDescXML = append([]byte(`<?xml version="1.0"?>`), s.rootDescXML...)
|
||||||
|
s.initMux(s.httpServeMux)
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UPnPService is the interface for the SOAP service.
|
||||||
|
type UPnPService interface {
|
||||||
|
Handle(action string, argsXML []byte, r *http.Request) (respArgs map[string]string, err error)
|
||||||
|
Subscribe(callback []*url.URL, timeoutSeconds int) (sid string, actualTimeout int, err error)
|
||||||
|
Unsubscribe(sid string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// initServicesMap is called during initialization of the server to prepare some internal datastructures.
|
||||||
|
func (s *server) initServicesMap() {
|
||||||
|
urn, err := upnp.ParseServiceType(services[0].ServiceType)
|
||||||
|
if err != nil {
|
||||||
|
// The service type is hardcoded, so this error should never happen.
|
||||||
|
log.Panicf("ParseServiceType: %v", err)
|
||||||
|
}
|
||||||
|
s.services = map[string]UPnPService{
|
||||||
|
urn.Type: &contentDirectoryService{
|
||||||
|
server: s,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// listInterfaces is called during initialization of the server to list the network interfaces
|
||||||
|
// on the machine.
|
||||||
|
func (s *server) listInterfaces() {
|
||||||
|
ifs, err := net.Interfaces()
|
||||||
|
if err != nil {
|
||||||
|
fs.Errorf(s.f, "list network interfaces: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var tmp []net.Interface
|
||||||
|
for _, intf := range ifs {
|
||||||
|
if intf.Flags&net.FlagUp == 0 || intf.MTU <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.Interfaces = append(s.Interfaces, intf)
|
||||||
|
tmp = append(tmp, intf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) initMux(mux *http.ServeMux) {
|
||||||
|
mux.HandleFunc(resPath, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
remotePath := r.URL.Query().Get("path")
|
||||||
|
node, err := s.vfs.Stat(remotePath)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Length", strconv.FormatInt(node.Size(), 10))
|
||||||
|
|
||||||
|
file := node.(*vfs.File)
|
||||||
|
in, err := file.Open(os.O_RDONLY)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
defer fs.CheckClose(in, &err)
|
||||||
|
|
||||||
|
http.ServeContent(w, r, remotePath, node.ModTime(), in)
|
||||||
|
return
|
||||||
|
})
|
||||||
|
|
||||||
|
mux.HandleFunc(rootDescPath, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("content-type", `text/xml; charset="utf-8"`)
|
||||||
|
w.Header().Set("content-length", fmt.Sprint(len(s.rootDescXML)))
|
||||||
|
w.Header().Set("server", serverField)
|
||||||
|
_, err := w.Write(s.rootDescXML)
|
||||||
|
if err != nil {
|
||||||
|
fs.Errorf(s, "Failed to serve root descriptor XML: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Install handlers to serve SCPD for each UPnP service.
|
||||||
|
for _, s := range services {
|
||||||
|
p := path.Join("/scpd", s.ServiceId)
|
||||||
|
s.SCPDURL = p
|
||||||
|
|
||||||
|
mux.HandleFunc(s.SCPDURL, func(serviceDesc string) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("content-type", `text/xml; charset="utf-8"`)
|
||||||
|
http.ServeContent(w, r, ".xml", time.Time{}, bytes.NewReader([]byte(serviceDesc)))
|
||||||
|
}
|
||||||
|
}(s.SCPD))
|
||||||
|
}
|
||||||
|
|
||||||
|
mux.HandleFunc(serviceControlURL, s.serviceControlHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle a service control HTTP request.
|
||||||
|
func (s *server) serviceControlHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
soapActionString := r.Header.Get("SOAPACTION")
|
||||||
|
soapAction, err := upnp.ParseActionHTTPHeader(soapActionString)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var env soap.Envelope
|
||||||
|
if err := xml.NewDecoder(r.Body).Decode(&env); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", `text/xml; charset="utf-8"`)
|
||||||
|
w.Header().Set("Ext", "")
|
||||||
|
w.Header().Set("server", serverField)
|
||||||
|
soapRespXML, code := func() ([]byte, int) {
|
||||||
|
respArgs, err := s.soapActionResponse(soapAction, env.Body.Action, r)
|
||||||
|
if err != nil {
|
||||||
|
upnpErr := upnp.ConvertError(err)
|
||||||
|
return mustMarshalXML(soap.NewFault("UPnPError", upnpErr)), 500
|
||||||
|
}
|
||||||
|
return marshalSOAPResponse(soapAction, respArgs), 200
|
||||||
|
}()
|
||||||
|
bodyStr := fmt.Sprintf(`<?xml version="1.0" encoding="utf-8" standalone="yes"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body>%s</s:Body></s:Envelope>`, soapRespXML)
|
||||||
|
w.WriteHeader(code)
|
||||||
|
if _, err := w.Write([]byte(bodyStr)); err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle a SOAP request and return the response arguments or UPnP error.
|
||||||
|
func (s *server) soapActionResponse(sa upnp.SoapAction, actionRequestXML []byte, r *http.Request) (map[string]string, error) {
|
||||||
|
service, ok := s.services[sa.Type]
|
||||||
|
if !ok {
|
||||||
|
// TODO: What's the invalid service error?
|
||||||
|
return nil, upnp.Errorf(upnp.InvalidActionErrorCode, "Invalid service: %s", sa.Type)
|
||||||
|
}
|
||||||
|
return service.Handle(sa.Action, actionRequestXML, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve runs the server - returns the error only if
|
||||||
|
// the listener was not started; does not block, so
|
||||||
|
// use s.Wait() to block on the listener indefinitely.
|
||||||
|
func (s *server) Serve() (err error) {
|
||||||
|
if s.HTTPConn == nil {
|
||||||
|
s.HTTPConn, err = net.Listen("tcp", s.httpListenAddr)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
s.startSSDP()
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
fs.Logf(s.f, "Serving HTTP on %s", s.HTTPConn.Addr().String())
|
||||||
|
|
||||||
|
err = s.serveHTTP()
|
||||||
|
if err != nil {
|
||||||
|
fs.Logf(s.f, "Error on serving HTTP server: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait blocks while the listener is open.
|
||||||
|
func (s *server) Wait() {
|
||||||
|
<-s.waitChan
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) Close() {
|
||||||
|
err := s.HTTPConn.Close()
|
||||||
|
if err != nil {
|
||||||
|
fs.Errorf(s.f, "Error closing HTTP server: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
close(s.waitChan)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run SSDP (multicast for server discovery) on all interfaces.
|
||||||
|
func (s *server) startSSDP() {
|
||||||
|
active := 0
|
||||||
|
stopped := make(chan struct{})
|
||||||
|
for _, intf := range s.Interfaces {
|
||||||
|
active++
|
||||||
|
go func(intf2 net.Interface) {
|
||||||
|
defer func() {
|
||||||
|
stopped <- struct{}{}
|
||||||
|
}()
|
||||||
|
s.ssdpInterface(intf2)
|
||||||
|
}(intf)
|
||||||
|
}
|
||||||
|
for active > 0 {
|
||||||
|
<-stopped
|
||||||
|
active--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run SSDP server on an interface.
|
||||||
|
func (s *server) ssdpInterface(intf net.Interface) {
|
||||||
|
// Figure out which HTTP location to advertise based on the interface IP.
|
||||||
|
advertiseLocationFn := func(ip net.IP) string {
|
||||||
|
url := url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: (&net.TCPAddr{
|
||||||
|
IP: ip,
|
||||||
|
Port: s.HTTPConn.Addr().(*net.TCPAddr).Port,
|
||||||
|
}).String(),
|
||||||
|
Path: rootDescPath,
|
||||||
|
}
|
||||||
|
return url.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
ssdpServer := ssdp.Server{
|
||||||
|
Interface: intf,
|
||||||
|
Devices: devices(),
|
||||||
|
Services: serviceTypes(),
|
||||||
|
Location: advertiseLocationFn,
|
||||||
|
Server: serverField,
|
||||||
|
UUID: s.rootDeviceUUID,
|
||||||
|
NotifyInterval: s.AnnounceInterval,
|
||||||
|
}
|
||||||
|
|
||||||
|
// An interface with these flags should be valid for SSDP.
|
||||||
|
const ssdpInterfaceFlags = net.FlagUp | net.FlagMulticast
|
||||||
|
|
||||||
|
if err := ssdpServer.Init(); err != nil {
|
||||||
|
if intf.Flags&ssdpInterfaceFlags != ssdpInterfaceFlags {
|
||||||
|
// Didn't expect it to work anyway.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.Contains(err.Error(), "listen") {
|
||||||
|
// OSX has a lot of dud interfaces. Failure to create a socket on
|
||||||
|
// the interface are what we're expecting if the interface is no
|
||||||
|
// good.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("Error creating ssdp server on %s: %s", intf.Name, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer ssdpServer.Close()
|
||||||
|
log.Println("Started SSDP on", intf.Name)
|
||||||
|
stopped := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(stopped)
|
||||||
|
if err := ssdpServer.Serve(); err != nil {
|
||||||
|
log.Printf("%q: %q\n", intf.Name, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case <-s.waitChan:
|
||||||
|
// Returning will close the server.
|
||||||
|
case <-stopped:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) serveHTTP() error {
|
||||||
|
srv := &http.Server{
|
||||||
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
s.httpServeMux.ServeHTTP(w, r)
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
err := srv.Serve(s.HTTPConn)
|
||||||
|
select {
|
||||||
|
case <-s.waitChan:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
88
cmd/serve/dlna/dlna_test.go
Normal file
88
cmd/serve/dlna/dlna_test.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
// +build go1.8
|
||||||
|
|
||||||
|
package dlna
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ncw/rclone/vfs"
|
||||||
|
|
||||||
|
_ "github.com/ncw/rclone/backend/local"
|
||||||
|
"github.com/ncw/rclone/cmd/serve/dlna/dlnaflags"
|
||||||
|
"github.com/ncw/rclone/fs"
|
||||||
|
"github.com/ncw/rclone/fs/config"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
dlnaServer *server
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
testBindAddress = "localhost:51777"
|
||||||
|
testURL = "http://" + testBindAddress + "/"
|
||||||
|
)
|
||||||
|
|
||||||
|
func startServer(t *testing.T, f fs.Fs) {
|
||||||
|
opt := dlnaflags.DefaultOpt
|
||||||
|
opt.ListenAddr = testBindAddress
|
||||||
|
dlnaServer = newServer(f, &opt)
|
||||||
|
assert.NoError(t, dlnaServer.Serve())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInit(t *testing.T) {
|
||||||
|
config.LoadConfig()
|
||||||
|
|
||||||
|
f, err := fs.NewFs("testdata/files")
|
||||||
|
l, _ := f.List("")
|
||||||
|
fmt.Println(l)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
startServer(t, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure that it serves rootDesc.xml (SCPD in uPnP parlance).
|
||||||
|
func TestRootSCPD(t *testing.T) {
|
||||||
|
req, err := http.NewRequest("GET", testURL+"rootDesc.xml", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
// Make sure that the SCPD contains a CDS service.
|
||||||
|
require.Contains(t, string(body),
|
||||||
|
"<serviceType>urn:schemas-upnp-org:service:ContentDirectory:1</serviceType>")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure that it serves content from the remote.
|
||||||
|
func TestServeContent(t *testing.T) {
|
||||||
|
itemPath := "/small_jpeg.jpg"
|
||||||
|
pathQuery := url.QueryEscape(itemPath)
|
||||||
|
req, err := http.NewRequest("GET", testURL+"res?path="+pathQuery, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer fs.CheckClose(resp.Body, &err)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
actualContents, err := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Now compare the contents with the golden file.
|
||||||
|
node, err := dlnaServer.vfs.Stat(itemPath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
goldenFile := node.(*vfs.File)
|
||||||
|
goldenReader, err := goldenFile.Open(os.O_RDONLY)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer fs.CheckClose(goldenReader, &err)
|
||||||
|
goldenContents, err := ioutil.ReadAll(goldenReader)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, goldenContents, actualContents)
|
||||||
|
}
|
52
cmd/serve/dlna/dlna_util.go
Normal file
52
cmd/serve/dlna/dlna_util.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package dlna
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/anacrolix/dms/soap"
|
||||||
|
"github.com/anacrolix/dms/upnp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeDeviceUUID(unique string) string {
|
||||||
|
h := md5.New()
|
||||||
|
if _, err := io.WriteString(h, unique); err != nil {
|
||||||
|
log.Panicf("makeDeviceUUID write failed: %s", err)
|
||||||
|
}
|
||||||
|
buf := h.Sum(nil)
|
||||||
|
return upnp.FormatUUID(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func didlLite(chardata string) string {
|
||||||
|
return `<DIDL-Lite` +
|
||||||
|
` xmlns:dc="http://purl.org/dc/elements/1.1/"` +
|
||||||
|
` xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"` +
|
||||||
|
` xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"` +
|
||||||
|
` xmlns:dlna="urn:schemas-dlna-org:metadata-1-0/">` +
|
||||||
|
chardata +
|
||||||
|
`</DIDL-Lite>`
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustMarshalXML(value interface{}) []byte {
|
||||||
|
ret, err := xml.MarshalIndent(value, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("mustMarshalXML failed to marshal %v: %s", value, err)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal SOAP response arguments into a response XML snippet.
|
||||||
|
func marshalSOAPResponse(sa upnp.SoapAction, args map[string]string) []byte {
|
||||||
|
soapArgs := make([]soap.Arg, 0, len(args))
|
||||||
|
for argName, value := range args {
|
||||||
|
soapArgs = append(soapArgs, soap.Arg{
|
||||||
|
XMLName: xml.Name{Local: argName},
|
||||||
|
Value: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return []byte(fmt.Sprintf(`<u:%[1]sResponse xmlns:u="%[2]s">%[3]s</u:%[1]sResponse>`,
|
||||||
|
sa.Action, sa.ServiceURN.String(), mustMarshalXML(soapArgs)))
|
||||||
|
}
|
42
cmd/serve/dlna/dlnaflags/dlnaflags.go
Normal file
42
cmd/serve/dlna/dlnaflags/dlnaflags.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package dlnaflags
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ncw/rclone/fs/config/flags"
|
||||||
|
"github.com/ncw/rclone/fs/rc"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Help contains the text for the command line help and manual.
|
||||||
|
var Help = `
|
||||||
|
### Server options
|
||||||
|
|
||||||
|
Use --addr to specify which IP address and port the server should
|
||||||
|
listen on, eg --addr 1.2.3.4:8000 or --addr :8080 to listen to all
|
||||||
|
IPs.
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
// Options is the type for DLNA serving options.
|
||||||
|
type Options struct {
|
||||||
|
ListenAddr string
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultOpt contains the defaults options for DLNA serving.
|
||||||
|
var DefaultOpt = Options{
|
||||||
|
ListenAddr: ":7879",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opt contains the options for DLNA serving.
|
||||||
|
var (
|
||||||
|
Opt = DefaultOpt
|
||||||
|
)
|
||||||
|
|
||||||
|
func addFlagsPrefix(flagSet *pflag.FlagSet, prefix string, Opt *Options) {
|
||||||
|
rc.AddOption("dlna", &Opt)
|
||||||
|
flags.StringVarP(flagSet, &Opt.ListenAddr, prefix+"addr", "", Opt.ListenAddr, "ip:port or :port to bind the DLNA http server to.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFlags add the command line flags for DLNA serving.
|
||||||
|
func AddFlags(flagSet *pflag.FlagSet) {
|
||||||
|
addFlagsPrefix(flagSet, "", &Opt)
|
||||||
|
}
|
BIN
cmd/serve/dlna/testdata/files/small_jpeg.jpg
vendored
Normal file
BIN
cmd/serve/dlna/testdata/files/small_jpeg.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 107 B |
@ -3,6 +3,8 @@ package serve
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"github.com/ncw/rclone/cmd/serve/dlna"
|
||||||
|
|
||||||
"github.com/ncw/rclone/cmd"
|
"github.com/ncw/rclone/cmd"
|
||||||
"github.com/ncw/rclone/cmd/serve/ftp"
|
"github.com/ncw/rclone/cmd/serve/ftp"
|
||||||
"github.com/ncw/rclone/cmd/serve/http"
|
"github.com/ncw/rclone/cmd/serve/http"
|
||||||
@ -19,6 +21,9 @@ func init() {
|
|||||||
if restic.Command != nil {
|
if restic.Command != nil {
|
||||||
Command.AddCommand(restic.Command)
|
Command.AddCommand(restic.Command)
|
||||||
}
|
}
|
||||||
|
if dlna.Command != nil {
|
||||||
|
Command.AddCommand(dlna.Command)
|
||||||
|
}
|
||||||
if ftp.Command != nil {
|
if ftp.Command != nil {
|
||||||
Command.AddCommand(ftp.Command)
|
Command.AddCommand(ftp.Command)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user